Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions daemon/test/codex/app-thread.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand Down
8 changes: 4 additions & 4 deletions daemon/test/codex/cxa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand All @@ -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');
Expand Down
8 changes: 4 additions & 4 deletions daemon/test/core/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<void>> = [];
afterEach(async () => { for (const c of cleanups.splice(0)) await c(); });

Expand Down Expand Up @@ -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<void>> = [];
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;
Expand Down
4 changes: 2 additions & 2 deletions daemon/test/integration/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] = [];
Expand Down
2 changes: 1 addition & 1 deletion daemon/test/system/hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions daemon/test/system/tmux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -156,4 +156,4 @@ describe.skipIf(!hasTmux)('tmux executor', () => {
]);
expect(stdout).toContain(`${name} 0`);
});
});
});
2 changes: 1 addition & 1 deletion daemon/test/system/wrappers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
18 changes: 12 additions & 6 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ rm ~/.deck-neo/sessions/probe.json
"keys": {
"approve": ["Enter"],
"reject": ["Escape"]
},
"launch": {
"claudeArgs": []
}
}
```
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
10 changes: 5 additions & 5 deletions scripts/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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,
),
Expand All @@ -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',
];
Expand Down