diff --git a/daemon/test/codex/app-thread.test.ts b/daemon/test/codex/app-thread.test.ts index f3a5944..5e7f6a8 100644 --- a/daemon/test/codex/app-thread.test.ts +++ b/daemon/test/codex/app-thread.test.ts @@ -5,9 +5,8 @@ // and its environment has no TMUX (verified with `lsof -d cwd` and `ps eww` on // the live process). Every notify program it spawns inherits that, and // basename('/') is '' — so neither of the two identity sources the adapter -// prefers can name the session. Four such turns were dropped on 2026-08-05 -// (~/.deck-neo/hook.log, 13:00:56Z–13:02:13Z). No codex turn may be silently -// dropped: an event that reaches the adapter has to land on some key. +// prefers can name the session. No Codex turn may be silently dropped: an +// event that reaches the adapter has to land on some key. import { readdir } from 'node:fs/promises'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; @@ -30,8 +29,8 @@ import { /** What the app-server (and anything else launched by launchd) reports as cwd. */ const APP_SERVER_CWD = '/'; const FAKE_TMUX = '/private/tmp/tmux-1000/default,1234,0'; -const THREAD_A = '019fd45d-4392-7b53-876b-f4d7b69fc1ec'; -const THREAD_B = '019fd32f-58f8-7450-a741-cb6078934f93'; +const THREAD_A = '00000000-0000-7000-8000-000000000001'; +const THREAD_B = '00000000-0000-7000-8000-000000000002'; let home: string; diff --git a/daemon/test/codex/cxa.test.ts b/daemon/test/codex/cxa.test.ts index 6e9d3da..4a3a4dd 100644 --- a/daemon/test/codex/cxa.test.ts +++ b/daemon/test/codex/cxa.test.ts @@ -53,8 +53,8 @@ describe('bin/cxa', () => { it('resumes the most recent indexed thread, title + id-hash, resume -- id, exact target', async () => { await writeIndex([ - { id: 'old-id', thread_name: 'Old thread', updated_at: '2026-08-01T00:00:00Z' }, - { id: '019f-abc', thread_name: 'Review the draft plan', updated_at: '2026-08-05T00:00:00Z' }, + { id: 'old-id', thread_name: 'Old thread', updated_at: '2030-01-01T00:00:00Z' }, + { id: '019f-abc', thread_name: 'Review the draft plan', updated_at: '2030-01-02T00:00:00Z' }, ]); const { dir, capture } = await tmuxShim(); await runCxa([], dir); @@ -112,7 +112,7 @@ describe('bin/cxa', () => { }); }); -describe('bin/cxa nested-tmux guard (F5)', () => { +describe('bin/cxa nested-tmux guard', () => { it('does not create a session when already inside tmux', async () => { const { dir, capture } = await tmuxShim(); // A fake codex on PATH so the exec target exists. @@ -126,7 +126,7 @@ describe('bin/cxa nested-tmux guard (F5)', () => { }); }); -describe('bin/cxa nested-tmux resume args (F-B)', () => { +describe('bin/cxa nested-tmux resume args', () => { it('resumes with -- and the id as SEPARATE args, not one glued word', async () => { const dir = await tmpDir('deckneo-cxaresume-'); const capture = join(dir, 'codex.argv'); diff --git a/daemon/test/core/store.test.ts b/daemon/test/core/store.test.ts index 4e348c4..1b94fab 100644 --- a/daemon/test/core/store.test.ts +++ b/daemon/test/core/store.test.ts @@ -453,7 +453,7 @@ describe('SessionStore.markWorking', () => { // Pins a known limitation rather than a desired behaviour: the reconcile sweep // re-reads every file whether or not it changed, so the override dies within // reconcileMs (2s in production) even though codex writes nothing until its - // turn ends. Flagged to the lead; change this test if the contract changes. + // turn ends. Change this test if the reconciliation contract changes. it('is reverted by the reconcile sweep even when no file was written', async () => { const { dir, store } = await setup({ reconcileMs: 40 }); await store.start(); @@ -665,7 +665,7 @@ describe('detached tmux sessions', () => { }); }); -describe('security: future-dated ts cannot pin a slot forever (F5)', () => { +describe('security: future-dated ts cannot pin a slot forever', () => { const cleanups: Array<() => Promise> = []; afterEach(async () => { for (const c of cleanups.splice(0)) await c(); }); @@ -709,12 +709,12 @@ describe('watch-only sessions age out faster (stale ChatGPT-app / no-tmux keys)' }); }); -describe('watch-only staleness only applies to terminal states (F6)', () => { +describe('watch-only staleness only applies to terminal states', () => { const cleanups: Array<() => Promise> = []; afterEach(async () => { for (const c of cleanups.splice(0)) await c(); }); it('keeps a WORKING watch-only session past the short window', async () => { - const dir = await makeTmpDir('deckneo-store-f6-'); + const dir = await makeTmpDir('deckneo-store-terminal-'); const store = new SessionStore(dir, { staleMs: HOUR, watchOnlyStaleMs: 1000 }); cleanups.push(async () => { await store.stop(); await rm(dir, { recursive: true, force: true }); }); const old = Date.now() - 5000; diff --git a/daemon/test/integration/app.test.ts b/daemon/test/integration/app.test.ts index eada4cc..ed2f77e 100644 --- a/daemon/test/integration/app.test.ts +++ b/daemon/test/integration/app.test.ts @@ -336,9 +336,9 @@ describe('deck-answered prompts flip blue immediately (all kinds)', () => { }); }); -describe('security: launch args cannot reach a shell (F2)', () => { +describe('security: launch args cannot reach a shell', () => { it('passes launch.claudeArgs as a separate argv array, never joined', async () => { - const dir = await mkdtemp(join(tmpdir(), 'deckneo-int-f2-')); + const dir = await mkdtemp(join(tmpdir(), 'deckneo-int-security-')); const store = new SessionStore(dir); const device = new FakeDevice(); const calls: unknown[][] = []; diff --git a/daemon/test/system/hook.test.ts b/daemon/test/system/hook.test.ts index 952de5c..91ff0ba 100644 --- a/daemon/test/system/hook.test.ts +++ b/daemon/test/system/hook.test.ts @@ -271,7 +271,7 @@ describe('idle-nag demotion (waiting-for-input is Ready, not amber)', () => { }); }); -describe('idle-nag must not clear a pending prompt (F2)', () => { +describe('idle-nag must not clear a pending prompt', () => { it('keeps a pending permission prompt amber when the idle nag arrives', async () => { await runHook('SessionStart', { session_id: 's', cwd: '/tmp/p' }, home); await runHook('Notification', { session_id: 's', message: 'Claude needs your permission to use Bash' }, home); diff --git a/daemon/test/system/tmux.test.ts b/daemon/test/system/tmux.test.ts index 3aadb24..fa11396 100644 --- a/daemon/test/system/tmux.test.ts +++ b/daemon/test/system/tmux.test.ts @@ -89,7 +89,7 @@ describe.skipIf(!hasTmux)('tmux executor', () => { expect(pane).toContain('$(touch /tmp/deckneo-should-not-exist) && echo pwned'); }); - it('sendText types dash-prefixed text literally, not as send-keys flags (F6)', async () => { + it('sendText types dash-prefixed text literally, not as send-keys flags', async () => { const s = await startScratch(); await tmuxSendText(s, '-X not-a-flag'); const pane = await capture(s); @@ -156,4 +156,4 @@ describe.skipIf(!hasTmux)('tmux executor', () => { ]); expect(stdout).toContain(`${name} 0`); }); -}); \ No newline at end of file +}); diff --git a/daemon/test/system/wrappers.test.ts b/daemon/test/system/wrappers.test.ts index 982df50..a229a54 100644 --- a/daemon/test/system/wrappers.test.ts +++ b/daemon/test/system/wrappers.test.ts @@ -89,7 +89,7 @@ describe('nested-tmux guard', () => { }); }); -describe('wrapper non-interactive passthrough (F1) and positional prompt (F3)', () => { +describe('wrapper non-interactive passthrough and positional prompt', () => { it('passes -p/--print/--version/--help straight to the real binary (needs no TTY)', async () => { for (const flag of ['-p', '--print', '--version', '--help']) { const { dir, captureOf } = await shims('claude', 'tmux'); diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 148731b..875076b 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -159,6 +159,9 @@ rm ~/.deck-neo/sessions/probe.json "keys": { "approve": ["Enter"], "reject": ["Escape"] + }, + "launch": { + "claudeArgs": [] } } ``` @@ -172,6 +175,9 @@ rm ~/.deck-neo/sessions/probe.json - `keys` — optional. Both default to the values shown; they are the literal `tmux send-keys` arguments used for approve and reject, so you can retune them (e.g. `["y", "Enter"]`) without touching code. +- `launch.claudeArgs` — optional. Each entry becomes a separate Claude CLI argument + when `+ NEW` starts a session. Keep the empty array unless you deliberately need + standing flags on every deck-launched session. The daemon reloads this file when it changes. If an edit is malformed it keeps the last good config and logs the parse error. @@ -293,12 +299,12 @@ that the paths are absolute, and check `~/.deck-neo/hook.log`. started with plain `claude`. Restart it with `cc`. **A key stays lit for a session I closed.** Slots free themselves when the session reports -`SessionEnd`, when its tmux session disappears (polled every 10 s — this applies only to -sessions started under tmux; a plain-`claude` session that dies without `SessionEnd` holds -its slot until the 24 h staleness rule), or when its state file's timestamp is older than -24 h. Freed sessions have their state files deleted automatically (ended sessions keep a -tombstone file for ~60 s so a straggling hook event cannot revive them). A stuck slot can -always be cleared by deleting its file from `~/.deck-neo/sessions/`. +`SessionEnd`, when its tmux session disappears (polled every 10 s), or when its state file +ages out. A watch-only session in `done` or `idle` expires after about 30 minutes; a +watch-only session still reporting active work can remain for up to 24 hours. Freed +sessions have their state files deleted automatically (ended sessions keep a tombstone +file for ~60 s so a straggling hook event cannot revive them). A stuck slot can always be +cleared by deleting its file from `~/.deck-neo/sessions/`. **Pressing a session key doesn't bring the window forward.** Window raising uses AppleScript via System Events, which needs Accessibility/Automation permission for the diff --git a/package.json b/package.json index fd6759f..e34c322 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "start": "tsx daemon/src/index.ts", "demo": "tsx scripts/demo.ts", + "preview": "tsx scripts/preview.ts", "test": "vitest run", "build": "tsc --noEmit" }, diff --git a/scripts/preview.ts b/scripts/preview.ts index e719b01..cd11ac6 100644 --- a/scripts/preview.ts +++ b/scripts/preview.ts @@ -11,8 +11,8 @@ const SCALE = 2; const cfg: DeckConfig = { projects: [ { name: 'deck_neo', path: '/tmp/deck_neo' }, - { name: 'chart_review', path: '/tmp/chart_review' }, - { name: 'flights', path: '/tmp/flights' }, + { name: 'api', path: '/tmp/api' }, + { name: 'web', path: '/tmp/web' }, ], commands: [ { label: 'CONTINUE', text: 'c' }, @@ -43,9 +43,9 @@ const row1 = keySpecsFor( cockpit( [ sess('deck_neo', 'working'), // watch-only working - sess('chart_review', 'needs-input', { tmux: 'chart_review' }), + sess('api', 'needs-input', { tmux: 'api' }), sess('api-2', 'done', { tmux: 'api-2', kind: 'codex' }), - sess('flights', 'idle', { tmux: 'flights' }), + sess('web', 'idle', { tmux: 'web' }), ], 1, ), @@ -67,7 +67,7 @@ const row2: KeySpec[] = [ const rows = [row1, row2]; const infobars = [ - 'chart_review ▸ Permission: Bash(npm test)', + 'api ▸ Permission: Bash(npm test)', 'api-2 ▸ codex ▸ done', 'no session selected', ];