@bastani/atomic 0.5.19-0 → 0.5.20-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,5 +21,17 @@
21
21
  "kotlin-lsp@claude-plugins-official": true,
22
22
  "swift-lsp@claude-plugins-official": true,
23
23
  "lua-lsp@claude-plugins-official": true
24
+ },
25
+ "hooks": {
26
+ "Stop": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "atomic _claude-stop-hook"
32
+ }
33
+ ]
34
+ }
35
+ ]
24
36
  }
25
37
  }
package/.mcp.json CHANGED
@@ -3,7 +3,9 @@
3
3
  "github": {
4
4
  "type": "http",
5
5
  "url": "https://api.githubcopilot.com/mcp",
6
- "headers": { "Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" }
6
+ "headers": {
7
+ "Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
8
+ }
7
9
  }
8
10
  }
9
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-picker-panel.d.ts","sourceRoot":"","sources":["../../../src/sdk/components/workflow-picker-panel.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workflow-picker-panel.d.ts","sourceRoot":"","sources":["../../../src/sdk/components/workflow-picker-panel.tsx"],"names":[],"mappings":"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"}
@@ -90,6 +90,55 @@ export declare function _hasUnresolvedHILTool(messages: SessionMessage[]): boole
90
90
  * reader are injected rather than hard-coded to `fs.watch` / `getSessionMessages`).
91
91
  */
92
92
  export declare function _runHILWatcher(events: AsyncIterable<unknown>, readMessages: () => Promise<SessionMessage[]>, onHIL: (waiting: boolean) => void): Promise<void>;
93
+ /**
94
+ * Path of the directory where the claude-stop-hook writes marker files.
95
+ * Each Claude turn creates `~/.atomic/claude-stop/<session_id>` atomically
96
+ * via rename, which triggers the `fs.watch` event in `waitForIdle`.
97
+ *
98
+ * @internal Exported for unit tests.
99
+ */
100
+ export declare function markerDir(): string;
101
+ /**
102
+ * Return the marker file path for a given Claude session ID.
103
+ *
104
+ * @internal Exported for unit tests.
105
+ */
106
+ export declare function markerPath(claudeSessionId: string): string;
107
+ /**
108
+ * Wait for the Claude session to become idle using `fs.watch` on the
109
+ * `~/.atomic/claude-stop/` marker directory.
110
+ *
111
+ * When Claude finishes a turn, the `atomic _claude-stop-hook` Stop hook writes
112
+ * `~/.atomic/claude-stop/<session_id>` atomically (tmp file + rename). The
113
+ * rename triggers an OS-native `fs.watch` event on the parent directory —
114
+ * far more reliable than polling tmux pane glyphs, which vary between Claude
115
+ * Code versions.
116
+ *
117
+ * Algorithm:
118
+ * 1. Watch the marker directory for events whose `filename` matches
119
+ * `claudeSessionId`.
120
+ * 2. On a matching event, read the session transcript via
121
+ * `getSessionMessages` and test `_hasUnresolvedHILTool`.
122
+ * - If unresolved HIL: call `onHIL(true)`, unlink the marker (so the next
123
+ * turn's hook can fire again), and continue watching.
124
+ * - If no unresolved HIL after a prior HIL: call `onHIL(false)`.
125
+ * - If truly idle: slice messages from `transcriptBeforeCount` and return.
126
+ * 3. Clean up the `fs.watch` watcher on any exit path via AbortController.
127
+ *
128
+ * The function signature is intentionally identical to the previous polling
129
+ * implementation so all callers remain unchanged.
130
+ *
131
+ * @param paneId - tmux pane (kept in signature for caller compat; not used here)
132
+ * @param claudeSessionId - Claude's session UUID (used to identify marker file)
133
+ * @param transcriptBeforeCount - number of messages in transcript before this turn
134
+ * @param beforeContent - (unused) pane content before send; kept for compat
135
+ * @param pollIntervalMs - (unused) kept for compat; watch is event-driven
136
+ * @param onHIL - optional callback for HIL state changes
137
+ */
138
+ /**
139
+ * @internal Exported for unit tests.
140
+ */
141
+ export declare function waitForIdle(_paneId: string, claudeSessionId: string | undefined, transcriptBeforeCount: number, _beforeContent: string, _pollIntervalMs: number, onHIL?: (waiting: boolean) => void): Promise<SessionMessage[]>;
93
142
  export interface ClaudeQueryOptions {
94
143
  /** tmux pane ID where Claude is running */
95
144
  paneId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/claude.ts"],"names":[],"mappings":"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"}
@@ -16,7 +16,7 @@ export interface AgentConfig {
16
16
  install_url: string;
17
17
  /** Paths to exclude when copying (relative to folder) */
18
18
  exclude: string[];
19
- /** Project files managed by `atomic init` for provider onboarding */
19
+ /** Project files applied during `atomic chat` preflight for provider onboarding */
20
20
  onboarding_files: Array<{
21
21
  source: string;
22
22
  destination: string;
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../../src/services/config/definitions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,qEAAqE;IACrE,gBAAgB,EAAE,KAAK,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,OAAO,CAAC;KAChB,CAAC,CAAC;CACJ;AAED,QAAA,MAAM,UAAU,4CAA6C,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,WAAW,CA2DtD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAEzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,WAAW,CAEzD;AAED,wBAAgB,YAAY,IAAI,QAAQ,EAAE,CAEzC"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../../src/services/config/definitions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mFAAmF;IACnF,gBAAgB,EAAE,KAAK,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,OAAO,CAAC;KAChB,CAAC,CAAC;CACJ;AAED,QAAA,MAAM,UAAU,4CAA6C,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,WAAW,CA2DtD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAEzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,WAAW,CAEzD;AAED,wBAAgB,YAAY,IAAI,QAAQ,EAAE,CAEzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.19-0",
3
+ "version": "0.5.20-0",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -283,6 +283,16 @@ Examples:
283
283
  process.exit(exitCode);
284
284
  });
285
285
 
286
+ // ── Internal: Claude Stop hook handler ────────────────────────────────
287
+ program
288
+ .command("_claude-stop-hook", { hidden: true })
289
+ .description("Internal: Claude Code Stop hook handler — writes a marker file for idle detection")
290
+ .action(async () => {
291
+ const { claudeStopHookCommand } = await import("./commands/cli/claude-stop-hook.ts");
292
+ const exitCode = await claudeStopHookCommand();
293
+ process.exit(exitCode);
294
+ });
295
+
286
296
  // ── Completions command ────────────────────────────────────────────────
287
297
  program
288
298
  .command("completions")
@@ -333,7 +343,8 @@ async function main(): Promise<void> {
333
343
  argv.includes("--help") ||
334
344
  argv.includes("-h") ||
335
345
  argv[0] === "completions" ||
336
- argv[0] === "_footer";
346
+ argv[0] === "_footer" ||
347
+ argv[0] === "_claude-stop-hook";
337
348
 
338
349
  if (!isInfoCommand) {
339
350
  const { autoSyncIfStale } = await import("./services/system/auto-sync.ts");
@@ -190,7 +190,15 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
190
190
  const args = await buildAgentArgs(agentType, passthroughArgs, projectRoot);
191
191
  const cmd = [config.cmd, ...args];
192
192
  const overrides = await getProviderOverrides(agentType, projectRoot);
193
- const envVars = { ...config.env_vars, ...overrides.envVars };
193
+ // ATOMIC_AGENT must be baked into the launcher env so the agent CLI
194
+ // (and anything it spawns) can read it. `setSessionEnv` below only
195
+ // affects processes spawned *after* the initial command, so it cannot
196
+ // populate the env of the agent CLI that `new-session` kicks off.
197
+ const envVars = {
198
+ ...config.env_vars,
199
+ ...overrides.envVars,
200
+ ATOMIC_AGENT: agentType,
201
+ };
194
202
 
195
203
  // ── No TTY: tmux attach requires a real terminal ──
196
204
  if (!process.stdin.isTTY) {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tests for claudeStopHookCommand.
3
+ *
4
+ * Strategy: monkey-patch `Bun.stdin.text` to return preset strings so we can
5
+ * call the function directly without spawning subprocesses. This is
6
+ * consistent with how other CLI-command tests in this directory work.
7
+ *
8
+ * Filesystem isolation: we use `crypto.randomUUID()` for unique session IDs
9
+ * and clean up in `afterEach` so test runs never collide with each other
10
+ * or with real marker files.
11
+ */
12
+
13
+ import { describe, test, expect, afterEach, mock, spyOn } from "bun:test";
14
+ import { access, rm } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import { homedir } from "node:os";
17
+ import { claudeStopHookCommand } from "./claude-stop-hook.ts";
18
+
19
+ // Paths we'll need in every test.
20
+ const markerDir = join(homedir(), ".atomic", "claude-stop");
21
+
22
+ /** Returns true when a file exists at `filePath`. */
23
+ async function fileExists(filePath: string): Promise<boolean> {
24
+ try {
25
+ await access(filePath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /** Patch `Bun.stdin.text` for the duration of one test. */
33
+ function mockStdin(text: string): void {
34
+ // Bun.stdin is a readonly property on the global `Bun` object.
35
+ // We reach it through the prototype chain the same way other tests
36
+ // in this repo patch globals (e.g. process.stdout.write).
37
+ (Bun.stdin as { text: () => Promise<string> }).text = () =>
38
+ Promise.resolve(text);
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Cleanup
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const sessionIdsToClean: string[] = [];
46
+
47
+ afterEach(async () => {
48
+ // Remove any marker files created during the test.
49
+ for (const id of sessionIdsToClean) {
50
+ await rm(join(markerDir, id), { force: true });
51
+ await rm(join(markerDir, `${id}.tmp`), { force: true });
52
+ }
53
+ sessionIdsToClean.length = 0;
54
+ });
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Tests
58
+ // ---------------------------------------------------------------------------
59
+
60
+ describe("claudeStopHookCommand", () => {
61
+ // 1. Valid payload → writes marker file
62
+ test("valid payload writes marker file and returns 0", async () => {
63
+ const sessionId = crypto.randomUUID();
64
+ sessionIdsToClean.push(sessionId);
65
+
66
+ mockStdin(JSON.stringify({ session_id: sessionId }));
67
+
68
+ const code = await claudeStopHookCommand();
69
+
70
+ expect(code).toBe(0);
71
+ expect(await fileExists(join(markerDir, sessionId))).toBe(true);
72
+ expect(await fileExists(join(markerDir, `${sessionId}.tmp`))).toBe(false);
73
+ });
74
+
75
+ // 2. stop_hook_active: true → no-op
76
+ test("stop_hook_active:true is a no-op and returns 0", async () => {
77
+ const sessionId = crypto.randomUUID();
78
+ sessionIdsToClean.push(sessionId);
79
+
80
+ mockStdin(
81
+ JSON.stringify({ session_id: sessionId, stop_hook_active: true }),
82
+ );
83
+
84
+ const code = await claudeStopHookCommand();
85
+
86
+ expect(code).toBe(0);
87
+ expect(await fileExists(join(markerDir, sessionId))).toBe(false);
88
+ expect(await fileExists(join(markerDir, `${sessionId}.tmp`))).toBe(false);
89
+ });
90
+
91
+ // 3. Malformed JSON → returns 0, logs to console.error
92
+ test("malformed JSON returns 0 and logs an error", async () => {
93
+ mockStdin("not json {{{");
94
+
95
+ // Spy on console.error so the error doesn't bleed into test output.
96
+ const errorSpy = spyOn(console, "error").mockImplementation(() => {});
97
+
98
+ const code = await claudeStopHookCommand();
99
+
100
+ expect(code).toBe(0);
101
+ expect(errorSpy).toHaveBeenCalled();
102
+
103
+ errorSpy.mockRestore();
104
+ });
105
+
106
+ // 4. Missing session_id → returns 0, logs to console.error
107
+ test("missing session_id returns 0 and logs an error", async () => {
108
+ mockStdin(JSON.stringify({}));
109
+
110
+ const errorSpy = spyOn(console, "error").mockImplementation(() => {});
111
+
112
+ const code = await claudeStopHookCommand();
113
+
114
+ expect(code).toBe(0);
115
+ expect(errorSpy).toHaveBeenCalled();
116
+
117
+ errorSpy.mockRestore();
118
+ });
119
+
120
+ // 5. Extra payload fields are tolerated
121
+ test("valid payload with optional fields writes marker and returns 0", async () => {
122
+ const sessionId = crypto.randomUUID();
123
+ sessionIdsToClean.push(sessionId);
124
+
125
+ mockStdin(
126
+ JSON.stringify({
127
+ session_id: sessionId,
128
+ transcript_path: "/tmp/transcript.json",
129
+ cwd: "/home/user/project",
130
+ stop_hook_active: false,
131
+ }),
132
+ );
133
+
134
+ const code = await claudeStopHookCommand();
135
+
136
+ expect(code).toBe(0);
137
+ expect(await fileExists(join(markerDir, sessionId))).toBe(true);
138
+ expect(await fileExists(join(markerDir, `${sessionId}.tmp`))).toBe(false);
139
+ });
140
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Claude Stop Hook command — internal handler for Claude Code's Stop hook.
3
+ *
4
+ * Claude invokes `atomic _claude-stop-hook` at the end of every turn,
5
+ * piping a JSON payload via stdin. This handler writes a marker file that
6
+ * another part of the system watches via `fs.watch`, replacing tmux-pane-
7
+ * scraping idle detection with a clean event-driven approach.
8
+ *
9
+ * Usage (configured in Claude's Stop hook):
10
+ * atomic _claude-stop-hook
11
+ *
12
+ * Payload (JSON via stdin):
13
+ * {
14
+ * "session_id": "abc123",
15
+ * "transcript_path": "/path/to/transcript",
16
+ * "cwd": "/path/to/cwd",
17
+ * "stop_hook_active": false
18
+ * }
19
+ */
20
+
21
+ import fs from "node:fs/promises";
22
+ import path from "node:path";
23
+ import os from "node:os";
24
+
25
+ /** Shape of the JSON payload Claude pipes to the Stop hook via stdin. */
26
+ export interface ClaudeStopHookPayload {
27
+ session_id: string;
28
+ transcript_path?: string;
29
+ cwd?: string;
30
+ stop_hook_active?: boolean;
31
+ }
32
+
33
+ /**
34
+ * Type guard to verify that a parsed value conforms to ClaudeStopHookPayload.
35
+ */
36
+ function isClaudeStopHookPayload(value: unknown): value is ClaudeStopHookPayload {
37
+ if (typeof value !== "object" || value === null) return false;
38
+ const obj = value as Record<string, unknown>;
39
+ if (typeof obj["session_id"] !== "string") return false;
40
+ if (obj["transcript_path"] !== undefined && typeof obj["transcript_path"] !== "string") return false;
41
+ if (obj["cwd"] !== undefined && typeof obj["cwd"] !== "string") return false;
42
+ if (obj["stop_hook_active"] !== undefined && typeof obj["stop_hook_active"] !== "boolean") return false;
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * Handler for the hidden `_claude-stop-hook` subcommand.
48
+ *
49
+ * Returns an exit code (0 on success or benign failure). The caller
50
+ * in src/cli.ts does `process.exit(exitCode)`, so we just return the code.
51
+ *
52
+ * We always return 0 — a non-zero exit would surface as a hook error in
53
+ * Claude's transcript, which is not what we want.
54
+ */
55
+ export async function claudeStopHookCommand(): Promise<number> {
56
+ // 1. Read stdin
57
+ const raw = await Bun.stdin.text();
58
+
59
+ // 2. Parse JSON
60
+ let payload: ClaudeStopHookPayload;
61
+ try {
62
+ const parsed: unknown = JSON.parse(raw);
63
+ if (!isClaudeStopHookPayload(parsed)) {
64
+ console.error("[claude-stop-hook] Invalid payload: missing or malformed 'session_id'");
65
+ return 0;
66
+ }
67
+ payload = parsed;
68
+ } catch {
69
+ console.error("[claude-stop-hook] Failed to parse stdin as JSON");
70
+ return 0;
71
+ }
72
+
73
+ // 3. Guard against infinite Stop-hook loops
74
+ if (payload.stop_hook_active === true) {
75
+ return 0;
76
+ }
77
+
78
+ // 4. Write the marker file atomically
79
+ const markerDir = path.join(os.homedir(), ".atomic", "claude-stop");
80
+ await fs.mkdir(markerDir, { recursive: true });
81
+
82
+ const tmpPath = path.join(markerDir, `${payload.session_id}.tmp`);
83
+ const finalPath = path.join(markerDir, payload.session_id);
84
+
85
+ // Write contents — the watcher only cares that the file appears.
86
+ await Bun.write(tmpPath, raw);
87
+ await fs.rename(tmpPath, finalPath);
88
+
89
+ return 0;
90
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Automatic project setup — replaces the interactive `atomic init` command.
2
+ * Automatic project setup.
3
3
  *
4
4
  * Applies onboarding files (MCP configs, settings). Called transparently
5
5
  * during `atomic chat` preflight so users never need to think about
@@ -572,6 +572,21 @@ function TextAreaContent({
572
572
  const onInputRef = useLatest(onInput);
573
573
  const lastTextRef = useRef(value);
574
574
 
575
+ // Read plainText on the next microtask so the textarea has applied the
576
+ // keystroke/paste before we observe its content, then fire onInput if
577
+ // it changed.
578
+ const flushPending = useCallback(() => {
579
+ queueMicrotask(() => {
580
+ const inst = instanceRef.current;
581
+ if (!inst) return;
582
+ const current = inst.plainText;
583
+ if (current !== lastTextRef.current) {
584
+ lastTextRef.current = current;
585
+ onInputRef.current(current);
586
+ }
587
+ });
588
+ }, []);
589
+
575
590
  const refCallback = useCallback((instance: TextareaRenderable | null) => {
576
591
  instanceRef.current = instance;
577
592
  }, []);
@@ -585,23 +600,14 @@ function TextAreaContent({
585
600
  }
586
601
  }, [value]);
587
602
 
588
- // Detect text changes after each keypress. The native Zig edit buffer's
589
- // "content-changed" event is unreliable for propagating to the JS
590
- // _contentChangeListener in certain installed environments. Instead,
591
- // we hook into useKeyboard (which fires before the textarea processes
592
- // the key) and defer the read with queueMicrotask so the textarea has
603
+ // flushPending fires on each keystroke via useKeyboard; onPaste handles
604
+ // bracketed pastes (which don't fire keydown). The native Zig edit
605
+ // buffer's "content-changed" event is unreliable for propagating to the
606
+ // JS _contentChangeListener in certain installed environments, so we
607
+ // hook into useKeyboard (which fires before the textarea processes the
608
+ // key) and defer the read with queueMicrotask so the textarea has
593
609
  // processed the keystroke by the time we read plainText.
594
- useKeyboard(useCallback(() => {
595
- queueMicrotask(() => {
596
- const inst = instanceRef.current;
597
- if (!inst) return;
598
- const current = inst.plainText;
599
- if (current !== lastTextRef.current) {
600
- lastTextRef.current = current;
601
- onInputRef.current(current);
602
- }
603
- });
604
- }, []));
610
+ useKeyboard(flushPending);
605
611
 
606
612
  return (
607
613
  <textarea
@@ -616,6 +622,7 @@ function TextAreaContent({
616
622
  placeholderColor={theme.textDim}
617
623
  wrapMode="word"
618
624
  flexGrow={1}
625
+ onPaste={flushPending}
619
626
  />
620
627
  );
621
628
  }
@@ -37,10 +37,11 @@ import {
37
37
  waitForPaneReady,
38
38
  attemptSubmitRounds,
39
39
  } from "../runtime/tmux.ts";
40
- import { watch } from "node:fs/promises";
40
+ import { watch, unlink, mkdir } from "node:fs/promises";
41
41
  import { existsSync, writeFileSync } from "node:fs";
42
42
  import { join } from "node:path";
43
43
  import { randomUUID } from "node:crypto";
44
+ import os from "node:os";
44
45
 
45
46
  // ---------------------------------------------------------------------------
46
47
  // Session tracking — ensures createClaudeSession is called before claudeQuery
@@ -346,95 +347,161 @@ export async function _runHILWatcher(
346
347
  // Helpers
347
348
  // ---------------------------------------------------------------------------
348
349
 
350
+ /**
351
+ * Path of the directory where the claude-stop-hook writes marker files.
352
+ * Each Claude turn creates `~/.atomic/claude-stop/<session_id>` atomically
353
+ * via rename, which triggers the `fs.watch` event in `waitForIdle`.
354
+ *
355
+ * @internal Exported for unit tests.
356
+ */
357
+ export function markerDir(): string {
358
+ return join(os.homedir(), ".atomic", "claude-stop");
359
+ }
360
+
361
+ /**
362
+ * Return the marker file path for a given Claude session ID.
363
+ *
364
+ * @internal Exported for unit tests.
365
+ */
366
+ export function markerPath(claudeSessionId: string): string {
367
+ return join(markerDir(), claudeSessionId);
368
+ }
369
+
370
+ /**
371
+ * Ensure the marker directory exists and remove any stale marker left from a
372
+ * previous turn of this session. Call this BEFORE submitting the prompt so
373
+ * the subsequent `waitForIdle` watch loop doesn't fire on a stale file.
374
+ *
375
+ * Ignores ENOENT on `unlink` — the file simply doesn't exist yet.
376
+ */
377
+ async function clearStaleMarker(claudeSessionId: string): Promise<void> {
378
+ await mkdir(markerDir(), { recursive: true });
379
+ try {
380
+ await unlink(markerPath(claudeSessionId));
381
+ } catch (e: unknown) {
382
+ // ENOENT is expected — ignore it; rethrow anything else
383
+ if (!(e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "ENOENT")) {
384
+ throw e;
385
+ }
386
+ }
387
+ }
388
+
349
389
  // ---------------------------------------------------------------------------
350
- // Idle detection via pane capture
390
+ // Idle detection via marker file watch
351
391
  // ---------------------------------------------------------------------------
352
392
 
353
393
  /**
354
- * Wait for the Claude session to become idle by polling the tmux pane.
394
+ * Wait for the Claude session to become idle using `fs.watch` on the
395
+ * `~/.atomic/claude-stop/` marker directory.
396
+ *
397
+ * When Claude finishes a turn, the `atomic _claude-stop-hook` Stop hook writes
398
+ * `~/.atomic/claude-stop/<session_id>` atomically (tmp file + rename). The
399
+ * rename triggers an OS-native `fs.watch` event on the parent directory —
400
+ * far more reliable than polling tmux pane glyphs, which vary between Claude
401
+ * Code versions.
355
402
  *
356
- * Interactive Claude Code sessions don't write idle or result events to the
357
- * JSONL session file (those only flow through the SDK streaming output for
358
- * headless consumers). The pane prompt indicator is the only reliable idle
359
- * signal for interactive sessions.
403
+ * Algorithm:
404
+ * 1. Watch the marker directory for events whose `filename` matches
405
+ * `claudeSessionId`.
406
+ * 2. On a matching event, read the session transcript via
407
+ * `getSessionMessages` and test `_hasUnresolvedHILTool`.
408
+ * - If unresolved HIL: call `onHIL(true)`, unlink the marker (so the next
409
+ * turn's hook can fire again), and continue watching.
410
+ * - If no unresolved HIL after a prior HIL: call `onHIL(false)`.
411
+ * - If truly idle: slice messages from `transcriptBeforeCount` and return.
412
+ * 3. Clean up the `fs.watch` watcher on any exit path via AbortController.
360
413
  *
361
- * Once idle is detected, assistant output is extracted from the session
362
- * transcript via `getSessionMessages()` rather than scraping the pane —
363
- * the transcript has structured content blocks, not terminal escape codes.
414
+ * The function signature is intentionally identical to the previous polling
415
+ * implementation so all callers remain unchanged.
364
416
  *
365
- * No timeout is imposed. The loop runs until the pane shows the idle prompt.
417
+ * @param paneId - tmux pane (kept in signature for caller compat; not used here)
418
+ * @param claudeSessionId - Claude's session UUID (used to identify marker file)
419
+ * @param transcriptBeforeCount - number of messages in transcript before this turn
420
+ * @param beforeContent - (unused) pane content before send; kept for compat
421
+ * @param pollIntervalMs - (unused) kept for compat; watch is event-driven
422
+ * @param onHIL - optional callback for HIL state changes
366
423
  */
367
- async function waitForIdle(
368
- paneId: string,
424
+ /**
425
+ * @internal Exported for unit tests.
426
+ */
427
+ export async function waitForIdle(
428
+ _paneId: string,
369
429
  claudeSessionId: string | undefined,
370
430
  transcriptBeforeCount: number,
371
- beforeContent: string,
372
- pollIntervalMs: number,
431
+ _beforeContent: string,
432
+ _pollIntervalMs: number,
373
433
  onHIL?: (waiting: boolean) => void,
374
434
  ): Promise<SessionMessage[]> {
375
- // Give Claude time to start processing before first poll
376
- await Bun.sleep(3_000);
435
+ // Without a session ID we cannot watch the marker directory — return empty.
436
+ if (!claudeSessionId) {
437
+ return [];
438
+ }
377
439
 
378
- let hilActive = false;
440
+ const dir = markerDir();
441
+ const sessionId = claudeSessionId;
442
+ const ac = new AbortController();
379
443
 
380
- while (true) {
381
- const currentContent = normalizeTmuxLines(capturePaneScrollback(paneId));
382
-
383
- // Must have new content compared to before we sent
384
- if (currentContent !== beforeContent) {
385
- const visible = capturePaneVisible(paneId);
386
- if (paneLooksReady(visible) && !paneHasActiveTask(visible)) {
387
- // Pane looks idle — but it might be waiting for user input (HIL).
388
- // Check the transcript for an unresolved AskUserQuestion before
389
- // treating this as a true completion.
390
- if (claudeSessionId) {
391
- try {
392
- const msgs = await getSessionMessages(claudeSessionId, {
393
- dir: process.cwd(),
394
- includeSystemMessages: true,
395
- });
396
-
397
- if (_hasUnresolvedHILTool(msgs)) {
398
- // Agent is blocked on user input — signal HIL and keep waiting
399
- if (!hilActive && onHIL) {
400
- onHIL(true);
401
- hilActive = true;
402
- }
403
- await Bun.sleep(pollIntervalMs);
404
- continue;
405
- }
444
+ let hilActive = false;
406
445
 
407
- // HIL was active but is now resolved — signal resumption
408
- if (hilActive && onHIL) {
409
- onHIL(false);
410
- hilActive = false;
411
- // Agent may still be processing after HIL resolution — keep
412
- // polling instead of returning immediately
413
- await Bun.sleep(pollIntervalMs);
414
- continue;
415
- }
446
+ try {
447
+ for await (const event of watch(dir, { signal: ac.signal })) {
448
+ // Filter: only care about events for our session's marker file
449
+ if (event.filename !== sessionId) continue;
450
+
451
+ // Marker appeared read transcript
452
+ let msgs: SessionMessage[];
453
+ try {
454
+ msgs = await getSessionMessages(sessionId, {
455
+ dir: process.cwd(),
456
+ includeSystemMessages: true,
457
+ });
458
+ } catch {
459
+ // Transcript read failed — wait for the next marker event
460
+ continue;
461
+ }
416
462
 
417
- // Truly idle — return transcript messages from this turn
418
- if (msgs.length > transcriptBeforeCount) {
419
- return msgs.slice(transcriptBeforeCount);
420
- }
421
- } catch {
422
- // Transcript read failed — return empty
423
- }
463
+ if (_hasUnresolvedHILTool(msgs)) {
464
+ // Agent is blocked on user input (HIL).
465
+ if (!hilActive) {
466
+ onHIL?.(true);
467
+ hilActive = true;
424
468
  }
425
- return [];
426
- } else if (hilActive) {
427
- // Pane is active again (user responded, agent resumed processing).
428
- // Clear HIL state.
429
- if (onHIL) {
430
- onHIL(false);
431
- hilActive = false;
469
+ // Remove the marker so the Stop hook can write a new one after the
470
+ // user responds and Claude finishes its next turn.
471
+ try {
472
+ await unlink(markerPath(sessionId));
473
+ } catch {
474
+ // ENOENT is fine — ignore
432
475
  }
476
+ // Continue watching for the next marker event
477
+ continue;
433
478
  }
434
- }
435
479
 
436
- await Bun.sleep(pollIntervalMs);
480
+ // No unresolved HIL — if we were in HIL state, signal resolution.
481
+ if (hilActive) {
482
+ onHIL?.(false);
483
+ hilActive = false;
484
+ }
485
+
486
+ // Truly idle — return transcript messages produced during this turn.
487
+ const result = msgs.length > transcriptBeforeCount
488
+ ? msgs.slice(transcriptBeforeCount)
489
+ : [];
490
+
491
+ ac.abort();
492
+ return result;
493
+ }
494
+ } catch (e: unknown) {
495
+ // AbortError is expected when we call ac.abort() to stop watching.
496
+ if (e instanceof Error && e.name === "AbortError") {
497
+ // Normal exit — return value was already set and returned above.
498
+ // If we somehow reach here without returning, fall through to [].
499
+ } else {
500
+ throw e;
501
+ }
437
502
  }
503
+
504
+ return [];
438
505
  }
439
506
 
440
507
  // ---------------------------------------------------------------------------
@@ -542,6 +609,11 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<SessionM
542
609
  const dir = process.cwd();
543
610
  const claudeSessionId = paneState.claudeSessionId;
544
611
 
612
+ // ── Clear any stale marker left from a previous turn before submitting. ──
613
+ // This ensures `waitForIdle`'s watch loop doesn't fire on the marker written
614
+ // by the Stop hook at the end of the LAST turn instead of the current one.
615
+ await clearStaleMarker(claudeSessionId);
616
+
545
617
  // ── First query: spawn `claude --session-id <UUID> 'Read the prompt in <path>'`.
546
618
  // The prompt is delivered via Claude's Read tool on its first turn — no
547
619
  // paste-buffer, no submit retries. Subsequent queries fall through to the
@@ -330,8 +330,8 @@ export async function hasAtomicGlobalAgentConfigs(
330
330
  }
331
331
 
332
332
  /**
333
- * Verify-and-repair entrypoint for user-facing commands (`atomic init`,
334
- * `atomic chat`). If every bundled agent file is present at its
333
+ * Verify-and-repair entrypoint for user-facing commands (`atomic chat`).
334
+ * If every bundled agent file is present at its
335
335
  * destination, returns immediately without touching disk. Otherwise
336
336
  * runs a merge re-sync, which fills the missing files from the local
337
337
  * config data dir while leaving user-added files alone.
@@ -17,7 +17,7 @@ export interface AgentConfig {
17
17
  install_url: string;
18
18
  /** Paths to exclude when copying (relative to folder) */
19
19
  exclude: string[];
20
- /** Project files managed by `atomic init` for provider onboarding */
20
+ /** Project files applied during `atomic chat` preflight for provider onboarding */
21
21
  onboarding_files: Array<{
22
22
  source: string;
23
23
  destination: string;