agent-relay-server 0.127.3 → 0.127.4

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.
Files changed (147) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-DnXnkt4a.js → MessageBubble-D2Wzxied.js} +2 -2
  4. package/public/assets/{MessageBubble-DnXnkt4a.js.map → MessageBubble-D2Wzxied.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-D6CPEpQD.js → PlanGraphPanel-DH_xoWZi.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-D6CPEpQD.js.map → PlanGraphPanel-DH_xoWZi.js.map} +1 -1
  7. package/public/assets/{activity-ZPwHcPLK.js → activity-B-ozOwNq.js} +2 -2
  8. package/public/assets/{activity-ZPwHcPLK.js.map → activity-B-ozOwNq.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CT0E9LV1.js → agent-profiles-DZgjO25A.js} +2 -2
  10. package/public/assets/{agent-profiles-CT0E9LV1.js.map → agent-profiles-DZgjO25A.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-BmBR-9CH.js → agent-type-icon-B9cLaOoz.js} +2 -2
  12. package/public/assets/{agent-type-icon-BmBR-9CH.js.map → agent-type-icon-B9cLaOoz.js.map} +1 -1
  13. package/public/assets/agents-mXBLSY6O.js +2 -0
  14. package/public/assets/{agents-Dt5TncjC.js.map → agents-mXBLSY6O.js.map} +1 -1
  15. package/public/assets/{analytics-BZtRycyL.js → analytics-5Ize_UL5.js} +3 -3
  16. package/public/assets/{analytics-BZtRycyL.js.map → analytics-5Ize_UL5.js.map} +1 -1
  17. package/public/assets/{automation-yzUJbvT2.js → automation-CidMvW2e.js} +2 -2
  18. package/public/assets/{automation-yzUJbvT2.js.map → automation-CidMvW2e.js.map} +1 -1
  19. package/public/assets/{badge-BCvp7LK3.js → badge-BkNNvFNa.js} +2 -2
  20. package/public/assets/{badge-BCvp7LK3.js.map → badge-BkNNvFNa.js.map} +1 -1
  21. package/public/assets/{branch-state-badge-Cscjqf-E.js → branch-state-badge-DnS40KCb.js} +2 -2
  22. package/public/assets/{branch-state-badge-Cscjqf-E.js.map → branch-state-badge-DnS40KCb.js.map} +1 -1
  23. package/public/assets/{button-DyHjsHY3.js → button-DUaC2wSQ.js} +2 -2
  24. package/public/assets/{button-DyHjsHY3.js.map → button-DUaC2wSQ.js.map} +1 -1
  25. package/public/assets/{card-BoqYxpVW.js → card-r-yAV4cH.js} +2 -2
  26. package/public/assets/{card-BoqYxpVW.js.map → card-r-yAV4cH.js.map} +1 -1
  27. package/public/assets/{channels-DAtOnA6o.js → channels-MPO2FOBv.js} +2 -2
  28. package/public/assets/{channels-DAtOnA6o.js.map → channels-MPO2FOBv.js.map} +1 -1
  29. package/public/assets/chat-Ca4ERso5.js +2 -0
  30. package/public/assets/chat-Ca4ERso5.js.map +1 -0
  31. package/public/assets/{connectors-B2grC-hl.js → connectors-BbYdtRkW.js} +2 -2
  32. package/public/assets/{connectors-B2grC-hl.js.map → connectors-BbYdtRkW.js.map} +1 -1
  33. package/public/assets/copy-button-u463GPZR.js +2 -0
  34. package/public/assets/{copy-button-C67FUG3E.js.map → copy-button-u463GPZR.js.map} +1 -1
  35. package/public/assets/{display-aTnwsqQh.js → display-BNI-7rdq.js} +2 -2
  36. package/public/assets/{display-aTnwsqQh.js.map → display-BNI-7rdq.js.map} +1 -1
  37. package/public/assets/{dist-D5w04z0l.js → dist-5e_is3Xr.js} +2 -2
  38. package/public/assets/{dist-D5w04z0l.js.map → dist-5e_is3Xr.js.map} +1 -1
  39. package/public/assets/{dist-BD4Xc13v.js → dist-ActI2qb5.js} +2 -2
  40. package/public/assets/{dist-BD4Xc13v.js.map → dist-ActI2qb5.js.map} +1 -1
  41. package/public/assets/dist-B-FQ0-X3.js +2 -0
  42. package/public/assets/{dist-G8YBD-wp.js.map → dist-B-FQ0-X3.js.map} +1 -1
  43. package/public/assets/{dist-f6MW74eX.js → dist-BduURKro.js} +2 -2
  44. package/public/assets/{dist-f6MW74eX.js.map → dist-BduURKro.js.map} +1 -1
  45. package/public/assets/{dist-BFDfbfNC.js → dist-Bp0-OBcd.js} +2 -2
  46. package/public/assets/{dist-BFDfbfNC.js.map → dist-Bp0-OBcd.js.map} +1 -1
  47. package/public/assets/{dist-C_VOBSaK.js → dist-CJ7e7uqU.js} +2 -2
  48. package/public/assets/{dist-C_VOBSaK.js.map → dist-CJ7e7uqU.js.map} +1 -1
  49. package/public/assets/{dist-BNB3fTO-.js → dist-CYzOsD_n.js} +2 -2
  50. package/public/assets/{dist-BNB3fTO-.js.map → dist-CYzOsD_n.js.map} +1 -1
  51. package/public/assets/{dist-CQMQ5iku.js → dist-CpcQJrKV.js} +2 -2
  52. package/public/assets/{dist-CQMQ5iku.js.map → dist-CpcQJrKV.js.map} +1 -1
  53. package/public/assets/dist-DFgmDjLv.js +2 -0
  54. package/public/assets/{dist-CHKra2JV.js.map → dist-DFgmDjLv.js.map} +1 -1
  55. package/public/assets/{dist-DG75bvPp.js → dist-DrpSpMKL.js} +2 -2
  56. package/public/assets/{dist-DG75bvPp.js.map → dist-DrpSpMKL.js.map} +1 -1
  57. package/public/assets/dist-DzWoiN2z.js +2 -0
  58. package/public/assets/{dist-BJKxqIQd.js.map → dist-DzWoiN2z.js.map} +1 -1
  59. package/public/assets/{es2015-a6fysEVz.js → es2015-DNuudVtQ.js} +2 -2
  60. package/public/assets/{es2015-a6fysEVz.js.map → es2015-DNuudVtQ.js.map} +1 -1
  61. package/public/assets/formatted-body-BEI1zP7i.js +3 -0
  62. package/public/assets/{formatted-body-D5-1aiyB.js.map → formatted-body-BEI1zP7i.js.map} +1 -1
  63. package/public/assets/{formatted-body-impl-Do5NbJUA.js → formatted-body-impl-CNQ5sqwK.js} +2 -2
  64. package/public/assets/{formatted-body-impl-Do5NbJUA.js.map → formatted-body-impl-CNQ5sqwK.js.map} +1 -1
  65. package/public/assets/index-D2Mpzpft.js +26 -0
  66. package/public/assets/{index-BdNafiPz.js.map → index-D2Mpzpft.js.map} +1 -1
  67. package/public/assets/{input-Rc4oWv4q.js → input-BvNUEkDd.js} +2 -2
  68. package/public/assets/{input-Rc4oWv4q.js.map → input-BvNUEkDd.js.map} +1 -1
  69. package/public/assets/{insights-i6AEZT_a.js → insights-5rkMFRgf.js} +2 -2
  70. package/public/assets/{insights-i6AEZT_a.js.map → insights-5rkMFRgf.js.map} +1 -1
  71. package/public/assets/{integrations-9c6IdRU4.js → integrations-BEaqjfP4.js} +2 -2
  72. package/public/assets/{integrations-9c6IdRU4.js.map → integrations-BEaqjfP4.js.map} +1 -1
  73. package/public/assets/{lib-BZ6WPg2t.js → lib-BvrbNA2I.js} +2 -2
  74. package/public/assets/{lib-BZ6WPg2t.js.map → lib-BvrbNA2I.js.map} +1 -1
  75. package/public/assets/{lucide-react-NXHPIMsX.js → lucide-react-DD_zIava.js} +2 -2
  76. package/public/assets/{lucide-react-NXHPIMsX.js.map → lucide-react-DD_zIava.js.map} +1 -1
  77. package/public/assets/maintenance-CEiURaYZ.js +2 -0
  78. package/public/assets/{maintenance-CJ6wikpg.js.map → maintenance-CEiURaYZ.js.map} +1 -1
  79. package/public/assets/{managed-agents-JZn0xNJR.js → managed-agents-CetHyuQW.js} +4 -4
  80. package/public/assets/{managed-agents-JZn0xNJR.js.map → managed-agents-CetHyuQW.js.map} +1 -1
  81. package/public/assets/{markdown-preview-impl-BsvAkpR4.js → markdown-preview-impl-ZwGYvObh.js} +2 -2
  82. package/public/assets/{markdown-preview-impl-BsvAkpR4.js.map → markdown-preview-impl-ZwGYvObh.js.map} +1 -1
  83. package/public/assets/memory-BTm8XzCg.js +2 -0
  84. package/public/assets/{memory-BE9IwCdC.js.map → memory-BTm8XzCg.js.map} +1 -1
  85. package/public/assets/{messages-vXgdc3H4.js → messages-fJd17pzv.js} +2 -2
  86. package/public/assets/{messages-vXgdc3H4.js.map → messages-fJd17pzv.js.map} +1 -1
  87. package/public/assets/{orchestrators-CribhLg-.js → orchestrators-C5XQ2UYy.js} +2 -2
  88. package/public/assets/{orchestrators-CribhLg-.js.map → orchestrators-C5XQ2UYy.js.map} +1 -1
  89. package/public/assets/overview-Bhk-tqKc.js +2 -0
  90. package/public/assets/{overview-q3O9K9VK.js.map → overview-Bhk-tqKc.js.map} +1 -1
  91. package/public/assets/{pairs-BNOnd0IE.js → pairs-CBrszSZh.js} +2 -2
  92. package/public/assets/{pairs-BNOnd0IE.js.map → pairs-CBrszSZh.js.map} +1 -1
  93. package/public/assets/{projects-iDkxAGBW.js → projects-DXy9JviE.js} +2 -2
  94. package/public/assets/{projects-iDkxAGBW.js.map → projects-DXy9JviE.js.map} +1 -1
  95. package/public/assets/prompt-settings-DEQhAt2X.js +2 -0
  96. package/public/assets/{prompt-settings-Bdz_xCgD.js.map → prompt-settings-DEQhAt2X.js.map} +1 -1
  97. package/public/assets/{react-dom-8yaD9iJG.js → react-dom-CADlISeR.js} +2 -2
  98. package/public/assets/{react-dom-8yaD9iJG.js.map → react-dom-CADlISeR.js.map} +1 -1
  99. package/public/assets/{registry-DPcyGypi.js → registry-DsZILce1.js} +4 -4
  100. package/public/assets/{registry-DPcyGypi.js.map → registry-DsZILce1.js.map} +1 -1
  101. package/public/assets/{security-Z3l3u-Pl.js → security-C-wVZm5k.js} +2 -2
  102. package/public/assets/{security-Z3l3u-Pl.js.map → security-C-wVZm5k.js.map} +1 -1
  103. package/public/assets/{select-BX_lW49D.js → select-DsEGBLJk.js} +2 -2
  104. package/public/assets/{select-BX_lW49D.js.map → select-DsEGBLJk.js.map} +1 -1
  105. package/public/assets/settings-D3_r5IGA.js +6 -0
  106. package/public/assets/{settings-Y381jZt7.js.map → settings-D3_r5IGA.js.map} +1 -1
  107. package/public/assets/{store-D-Frk_EC.js → store-DKTb9O0v.js} +5 -5
  108. package/public/assets/{store-D-Frk_EC.js.map → store-DKTb9O0v.js.map} +1 -1
  109. package/public/assets/{task-chip-CeujtXYT.js → task-chip-DwdauC08.js} +2 -2
  110. package/public/assets/{task-chip-CeujtXYT.js.map → task-chip-DwdauC08.js.map} +1 -1
  111. package/public/assets/{tasks-Di6d0_Ch.js → tasks-DnEzBku4.js} +2 -2
  112. package/public/assets/{tasks-Di6d0_Ch.js.map → tasks-DnEzBku4.js.map} +1 -1
  113. package/public/assets/{teams-DJv4l-_l.js → teams-DpAbZ3ye.js} +2 -2
  114. package/public/assets/{teams-DJv4l-_l.js.map → teams-DpAbZ3ye.js.map} +1 -1
  115. package/public/assets/{terminal-viewer-impl-3zBLpRel.js → terminal-viewer-impl-_IU5o8ya.js} +3 -3
  116. package/public/assets/{terminal-viewer-impl-3zBLpRel.js.map → terminal-viewer-impl-_IU5o8ya.js.map} +1 -1
  117. package/public/assets/types-BqeSkWrv.js.map +1 -1
  118. package/public/assets/{use-keyboard-viewport-cGEmrGE7.js → use-keyboard-viewport-wTh5CR4-.js} +2 -2
  119. package/public/assets/{use-keyboard-viewport-cGEmrGE7.js.map → use-keyboard-viewport-wTh5CR4-.js.map} +1 -1
  120. package/public/assets/user-avatar-CZ3XO8ml.js +2 -0
  121. package/public/assets/{user-avatar-qbo-B3dQ.js.map → user-avatar-CZ3XO8ml.js.map} +1 -1
  122. package/public/assets/{work-queue-D73-hP0c.js → work-queue-DKcLZWRl.js} +2 -2
  123. package/public/assets/{work-queue-D73-hP0c.js.map → work-queue-DKcLZWRl.js.map} +1 -1
  124. package/public/assets/workspaces-Cs2IZZOI.js +3 -0
  125. package/public/assets/{workspaces-CoDz-iZd.js.map → workspaces-Cs2IZZOI.js.map} +1 -1
  126. package/public/index.html +24 -24
  127. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  128. package/runner/src/adapters/claude-transcript.ts +52 -3
  129. package/runner/src/control-server.ts +34 -17
  130. package/runner/src/runner-core.ts +64 -30
  131. package/src/db/messages.ts +20 -8
  132. package/public/assets/agents-Dt5TncjC.js +0 -2
  133. package/public/assets/chat-DhYX9BDG.js +0 -2
  134. package/public/assets/chat-DhYX9BDG.js.map +0 -1
  135. package/public/assets/copy-button-C67FUG3E.js +0 -2
  136. package/public/assets/dist-BJKxqIQd.js +0 -2
  137. package/public/assets/dist-CHKra2JV.js +0 -2
  138. package/public/assets/dist-G8YBD-wp.js +0 -2
  139. package/public/assets/formatted-body-D5-1aiyB.js +0 -3
  140. package/public/assets/index-BdNafiPz.js +0 -26
  141. package/public/assets/maintenance-CJ6wikpg.js +0 -2
  142. package/public/assets/memory-BE9IwCdC.js +0 -2
  143. package/public/assets/overview-q3O9K9VK.js +0 -2
  144. package/public/assets/prompt-settings-Bdz_xCgD.js +0 -2
  145. package/public/assets/settings-Y381jZt7.js +0 -6
  146. package/public/assets/user-avatar-qbo-B3dQ.js +0 -2
  147. package/public/assets/workspaces-CoDz-iZd.js +0 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.127.3",
4
+ "version": "0.127.4",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -25,6 +25,7 @@ export interface TurnStep {
25
25
  type: "narration" | "reasoning" | "tool";
26
26
  text: string;
27
27
  label?: string;
28
+ occurredAt?: number;
28
29
  }
29
30
 
30
31
  interface TranscriptMessage {
@@ -35,6 +36,7 @@ interface TranscriptMessage {
35
36
 
36
37
  interface TranscriptEntry {
37
38
  type?: string;
39
+ timestamp?: string;
38
40
  message?: TranscriptMessage;
39
41
  // Claude Code stamps every transcript entry with `isSidechain`: true for
40
42
  // entries belonging to a Task (subagent) run, false for the root session.
@@ -225,6 +227,46 @@ export function countTranscriptEntries(jsonl: string): number {
225
227
  return count;
226
228
  }
227
229
 
230
+ /** Key-order-independent structural equality, cheap enough for a small tool_input object. */
231
+ function canonicalJson(value: unknown): string {
232
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
233
+ if (value && typeof value === "object") {
234
+ const keys = Object.keys(value as Record<string, unknown>).sort();
235
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
236
+ }
237
+ return JSON.stringify(value);
238
+ }
239
+
240
+ /**
241
+ * True when the transcript already contains the exact assistant tool_use block that
242
+ * triggered a PreToolUse hook — the settle-poll anchor (#1116). The hook payload's own
243
+ * `tool_name`/`tool_input` are the precise call that fired it, so matching them against
244
+ * the transcript's tool_use blocks (name + structural input equality) is exact, unlike
245
+ * guessing from transcript shape alone. Claude's JSONL flush is async/bursty and can
246
+ * lag well behind the hook firing, so the entry backing the hook's own call may still be
247
+ * missing from disk the instant the hook runs — this is what the settle-poll waits for.
248
+ * #1086 should adopt this same anchor for its own turn-final settle marker rather than
249
+ * inventing a second one.
250
+ */
251
+ export function transcriptHasToolUseAnchor(jsonl: string, toolName: string, toolInput: unknown): boolean {
252
+ const wantInput = canonicalJson(toolInput ?? {});
253
+ for (const line of jsonl.split("\n")) {
254
+ const trimmed = line.trim();
255
+ if (!trimmed) continue;
256
+ let entry: TranscriptEntry;
257
+ try {
258
+ entry = JSON.parse(trimmed) as TranscriptEntry;
259
+ } catch {
260
+ continue;
261
+ }
262
+ if (entry.type !== "assistant" || isSidechainEntry(entry)) continue;
263
+ for (const block of blocks(entry.message)) {
264
+ if (block.type === "tool_use" && block.name === toolName && canonicalJson(block.input ?? {}) === wantInput) return true;
265
+ }
266
+ }
267
+ return false;
268
+ }
269
+
228
270
  /**
229
271
  * Extract the ordered narration, reasoning, and tool steps for the most recent
230
272
  * turn (since the last real user prompt). Used by the reasoning tailer to stream
@@ -283,18 +325,25 @@ export class LatestTurnStepAccumulator {
283
325
  return;
284
326
  }
285
327
  if (entry.type !== "assistant") return;
328
+ const occurredAt = transcriptEntryOccurredAt(entry);
286
329
  for (const b of blocks(entry.message)) {
287
330
  if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
288
- this.latestSteps.push({ type: "narration", text: b.text.trim() });
331
+ this.latestSteps.push({ type: "narration", text: b.text.trim(), ...(occurredAt ? { occurredAt } : {}) });
289
332
  } else if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim()) {
290
- this.latestSteps.push({ type: "reasoning", text: b.thinking.trim() });
333
+ this.latestSteps.push({ type: "reasoning", text: b.thinking.trim(), ...(occurredAt ? { occurredAt } : {}) });
291
334
  } else if (b.type === "tool_use" && typeof b.name === "string" && b.name) {
292
- this.latestSteps.push({ type: "tool", label: b.name, text: summarizeToolUse(b.name, b.input) });
335
+ this.latestSteps.push({ type: "tool", label: b.name, text: summarizeToolUse(b.name, b.input), ...(occurredAt ? { occurredAt } : {}) });
293
336
  }
294
337
  }
295
338
  }
296
339
  }
297
340
 
341
+ function transcriptEntryOccurredAt(entry: TranscriptEntry): number | undefined {
342
+ if (typeof entry.timestamp !== "string" || !entry.timestamp) return undefined;
343
+ const parsed = Date.parse(entry.timestamp);
344
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
345
+ }
346
+
298
347
  /**
299
348
  * Stable dedup keys for a turn's steps, in order. Each key is salted with how many
300
349
  * identical (type,label,text) steps preceded it in the same window — so running the
@@ -49,9 +49,13 @@ interface ControlServerOptions {
49
49
  // dashboard chat without the agent re-emitting it via /reply.
50
50
  // isPreFlush: true signals a mid-turn call from a PreToolUse hook — flush any
51
51
  // un-mirrored narrative before the interactive form appears in chat (#435).
52
+ // toolName/toolInput (#1116): the hook payload's own tool call — the exact anchor the
53
+ // runner settle-polls the transcript for before draining and returning. The resolved
54
+ // `reasoningSettled` reflects whether that anchor was actually found (vs a bounded
55
+ // timeout), so the interactive form can stamp an honest value instead of an assumed one.
52
56
  // promptId (#1086): Claude's native `prompt_id` from the Stop hook payload — the
53
57
  // stable, provider-native turn identifier used instead of a minted random UUID.
54
- onSessionTurn?(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean }): Promise<void>;
58
+ onSessionTurn?(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean; toolName?: string; toolInput?: unknown }): Promise<{ reasoningSettled: boolean } | void>;
55
59
  onPermissionResolved?(input: ResolvedPermissionPrompt): Promise<void> | void;
56
60
  // A provider UserPromptSubmit hook hands over the prompt the human typed
57
61
  // directly into the session (web terminal / TUI) so the runner can mirror it
@@ -84,6 +88,8 @@ export interface ResolvedPermissionPrompt {
84
88
  approvalId: string;
85
89
  decision: ProviderPermissionDecisionInput["decision"];
86
90
  decisionLabel: string;
91
+ occurredAt: number;
92
+ resolvedAt: number;
87
93
  questions?: unknown[];
88
94
  answers?: Record<string, string>;
89
95
  }
@@ -264,18 +270,27 @@ async function handlePermissionRequest(
264
270
  options: ControlServerOptions,
265
271
  pendingPermissionRequests: Map<string, { resolve(input: ProviderPermissionDecisionInput): void; timer: Timer }>,
266
272
  ): Promise<Response> {
273
+ const occurredAt = Date.now();
267
274
  const body = await req.json().catch(() => null);
268
275
  if (!isRecord(body)) return Response.json({});
269
- // #435: flush any un-mirrored narrative from the transcript before the form
270
- // appears in chat the transcript already has the assistant text block at
271
- // PreToolUse time, but the Stop hook (which normally captures it) won't fire
272
- // until after the user answers, so the form would otherwise arrive first.
276
+ // #435/#1116: flush any un-mirrored narrative from the transcript before the form
277
+ // appears in chat. The old comment here asserted the transcript already had the
278
+ // assistant text block at PreToolUse time empirically false: Claude's JSONL flush is
279
+ // async/bursty and can lag the hook firing by a long margin. onSessionTurn now
280
+ // settle-polls for the exact tool_use entry that triggered this hook (toolName/
281
+ // toolInput below are that anchor) before draining, and honestly reports back whether
282
+ // it found the anchor or hit its bounded timeout — reasoningSettled reflects that,
283
+ // rather than an assumption nothing here confirmed.
273
284
  const transcriptPath = typeof body.transcript_path === "string" ? body.transcript_path : "";
285
+ const toolName = typeof body.tool_name === "string" ? body.tool_name : undefined;
286
+ const toolInput = isRecord(body.tool_input) ? body.tool_input : undefined;
287
+ let reasoningSettled = true;
274
288
  if (transcriptPath && options.onSessionTurn) {
275
- await options.onSessionTurn({ transcriptPath, isPreFlush: true }).catch(() => {});
289
+ const result = await options.onSessionTurn({ transcriptPath, isPreFlush: true, toolName, toolInput }).catch(() => undefined);
290
+ if (result && typeof result.reasoningSettled === "boolean") reasoningSettled = result.reasoningSettled;
276
291
  }
277
292
  const approvalId = crypto.randomUUID();
278
- const view = claudePermissionApprovalView(approvalId, body);
293
+ const view = claudePermissionApprovalView(approvalId, body, reasoningSettled);
279
294
  options.onStatus({
280
295
  status: "busy",
281
296
  reason: "provider-turn",
@@ -299,18 +314,18 @@ async function handlePermissionRequest(
299
314
  });
300
315
 
301
316
  if (decision.reason !== PERMISSION_TIMEOUT_REASON && options.onPermissionResolved) {
302
- await Promise.resolve(options.onPermissionResolved(claudeResolvedPermissionPrompt(view, decision, body))).catch(() => {});
317
+ await Promise.resolve(options.onPermissionResolved(claudeResolvedPermissionPrompt(view, decision, body, occurredAt))).catch(() => {});
303
318
  }
304
319
 
305
320
  return Response.json(claudePermissionHookResponse(decision, body));
306
321
  }
307
322
 
308
323
  // Build the normalized InteractivePrompt for a Claude PreToolUse permission/question hook.
309
- // reasoningSettled is stamped `true` because handlePermissionRequest pre-flushes the
310
- // transcript narrative (onSessionTurn isPreFlush) BEFORE this view is built and emitted, so
311
- // the turn's reasoning is already mirrored ahead of the form (#435/#723). No `provider` tag —
312
- // the server reads the typed contract, not provider identity.
313
- function claudePermissionApprovalView(id: string, body: Record<string, unknown>): InteractivePrompt {
324
+ // reasoningSettled is the caller's settle-poll result (#435/#723/#1116): true when
325
+ // handlePermissionRequest's pre-flush confirmed the turn's reasoning was drained before
326
+ // this view was built, false when it hit its bounded timeout without confirming. No
327
+ // `provider` tag — the server reads the typed contract, not provider identity.
328
+ function claudePermissionApprovalView(id: string, body: Record<string, unknown>, reasoningSettled: boolean): InteractivePrompt {
314
329
  const toolName = typeof body.tool_name === "string" ? body.tool_name : "Tool";
315
330
  const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
316
331
  // AskUserQuestion is not a yes/no gate — it asks the user to pick answers.
@@ -326,7 +341,7 @@ function claudePermissionApprovalView(id: string, body: Record<string, unknown>)
326
341
  body: "",
327
342
  questions: toolInput.questions as InteractivePrompt["questions"],
328
343
  choices: [],
329
- reasoningSettled: true,
344
+ reasoningSettled,
330
345
  };
331
346
  }
332
347
  // ExitPlanMode arrives through the generic PermissionRequest hook (it doesn't
@@ -347,7 +362,7 @@ function claudePermissionApprovalView(id: string, body: Record<string, unknown>)
347
362
  { id: "approve", label: "Approve plan" },
348
363
  { id: "deny", label: "Keep planning" },
349
364
  ],
350
- reasoningSettled: true,
365
+ reasoningSettled,
351
366
  };
352
367
  }
353
368
  const command = typeof toolInput.command === "string" ? toolInput.command : "";
@@ -368,7 +383,7 @@ function claudePermissionApprovalView(id: string, body: Record<string, unknown>)
368
383
  { id: "deny", label: "Deny" },
369
384
  { id: "abort", label: "Abort" },
370
385
  ],
371
- reasoningSettled: true,
386
+ reasoningSettled,
372
387
  };
373
388
  }
374
389
 
@@ -437,7 +452,7 @@ function claudePermissionHookResponse(decision: ProviderPermissionDecisionInput,
437
452
  };
438
453
  }
439
454
 
440
- function claudeResolvedPermissionPrompt(view: InteractivePrompt, decision: ProviderPermissionDecisionInput, body: Record<string, unknown>): ResolvedPermissionPrompt {
455
+ function claudeResolvedPermissionPrompt(view: InteractivePrompt, decision: ProviderPermissionDecisionInput, body: Record<string, unknown>, occurredAt: number): ResolvedPermissionPrompt {
441
456
  const toolName = typeof body.tool_name === "string" ? body.tool_name : "Tool";
442
457
  const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
443
458
  const kind = view.kind === "questions" || view.kind === "plan" || view.kind === "command" || view.kind === "tool"
@@ -459,6 +474,8 @@ function claudeResolvedPermissionPrompt(view: InteractivePrompt, decision: Provi
459
474
  approvalId: decision.approvalId,
460
475
  decision: decision.decision,
461
476
  decisionLabel,
477
+ occurredAt,
478
+ resolvedAt: Date.now(),
462
479
  ...(questions ? { questions } : {}),
463
480
  ...(decision.answers ? { answers: decision.answers } : {}),
464
481
  };
@@ -16,7 +16,7 @@ import { readRunnerContextProbeState } from "./context-probe-state";
16
16
  import { startControlServer, type ControlServer, type ResolvedPermissionPrompt } from "./control-server";
17
17
  import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
18
18
  import { Outbox, type OutboxRecord } from "./outbox";
19
- import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, extractAssistantTurnForPrompt, countTranscriptEntries, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
19
+ import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, extractAssistantTurnForPrompt, countTranscriptEntries, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptHasToolUseAnchor, transcriptLooksComplete } from "./adapters/claude-transcript";
20
20
  import { IncrementalClaudeTranscriptTail } from "./adapters/claude-transcript-tail";
21
21
  import { getManifest } from "agent-relay-providers";
22
22
  import { profileUsesProviderHostGlobals } from "./profile-home";
@@ -153,6 +153,13 @@ const MAX_INITIAL_PROMPT_ATTEMPTS = 6;
153
153
  // transcript bytes.
154
154
  const REASONING_POLL_MS = 1_200;
155
155
  const SESSION_MIRROR_BATCH_MAX = 100;
156
+ // #1116: bounded settle-poll for the PreToolUse pre-flush anchor. Claude's JSONL flush is
157
+ // async/bursty and can lag the hook firing by a long margin, so a one-shot read (the old
158
+ // #435/#499 behavior) often finds nothing. Poll at this cadence until the exact triggering
159
+ // tool_use entry lands on disk, but never past the timeout — the question must never block
160
+ // the user more than a few seconds waiting on a transcript write that might not be coming.
161
+ const SETTLE_POLL_INTERVAL_MS = 250;
162
+ const SETTLE_POLL_TIMEOUT_MS = 4_000;
156
163
  interface RunnerTimelineEvent {
157
164
  status: string;
158
165
  id?: string;
@@ -285,6 +292,10 @@ export class AgentRunner {
285
292
  // Reasoning tailer (item 5): streams the in-flight turn's reasoning/tool steps
286
293
  // from the Claude transcript into chat as discreet session events.
287
294
  private reasoningTail?: ReasoningTailState;
295
+ // Settle-poll cadence for the pre-flush anchor (#1116) — instance fields (not just the
296
+ // module constants) so tests can shrink them without waiting out the real bound.
297
+ private settlePollIntervalMs = SETTLE_POLL_INTERVAL_MS;
298
+ private settlePollTimeoutMs = SETTLE_POLL_TIMEOUT_MS;
288
299
  private scratch?: SessionScratchLayout;
289
300
  // #417: for non-claude providers, tracks how many times each spawner obligation has
290
301
  // been re-injected so a non-responsive agent can't spin in a re-injection loop.
@@ -424,7 +435,7 @@ export class AgentRunner {
424
435
  // Hot-path-safe: answered instantly from the local snapshot, never a server
425
436
  // round-trip. The snapshot is kept warm by the background refresh below (#196).
426
437
  onReplyObligations: () => Promise.resolve(this.obligationCache.get()),
427
- onSessionTurn: (input) => this.publishSessionTurn(input), onPermissionResolved: (input) => this.publishSessionEvent({ from: this.agentId, to: "user", body: input.body, session: { type: "prompt-resolution", origin: "provider", label: input.decisionLabel, ...(this.currentTurnId ? { turnId: this.currentTurnId } : {}), stepId: `prompt-resolution:${input.approvalId}`, promptResolution: input } }),
438
+ onSessionTurn: (input) => this.publishSessionTurn(input), onPermissionResolved: (input) => this.publishSessionEvent({ from: this.agentId, to: "user", body: input.body, occurredAt: input.occurredAt, session: { type: "prompt-resolution", origin: "provider", label: input.decisionLabel, ...(this.currentTurnId ? { turnId: this.currentTurnId } : {}), stepId: `prompt-resolution:${input.approvalId}`, promptResolution: input } }),
428
439
  onUserPrompt: (input) => this.handleUserPrompt(input),
429
440
  onSessionBoundary: (input) => this.handleSessionBoundary(input),
430
441
  onHookFatal: (report) => this.reportHookFatal(report),
@@ -1435,17 +1446,21 @@ export class AgentRunner {
1435
1446
  // no relay message) are mirrored too. A reply obligation, when present, is still
1436
1447
  // used as replyTo so the Stop hook stops nagging the agent to /reply.
1437
1448
  //
1438
- // isPreFlush: true (#435/#499) — mid-turn PreToolUse boundary. Drain the live
1439
- // tail and force-flush session messages before showing the blocking control;
1440
- // the entry cursor lets Stop capture skip already-emitted text.
1441
- private async publishSessionTurn(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean }): Promise<void> {
1449
+ // isPreFlush: true (#435/#499/#1116) — mid-turn PreToolUse boundary. Settle-polls the
1450
+ // transcript for the exact tool_use entry that triggered the hook (toolName/toolInput,
1451
+ // the hook payload's own anchor), then drains the live tail and force-flushes session
1452
+ // messages before showing the blocking control; the entry cursor lets Stop capture skip
1453
+ // already-emitted text. Returns whether the settle-poll actually found the anchor
1454
+ // (reasoningSettled) — an honest `false` on timeout rather than a stamped `true` on a
1455
+ // premise nothing here confirmed (#1116; see InteractivePrompt.reasoningSettled).
1456
+ private async publishSessionTurn(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean; toolName?: string; toolInput?: unknown }): Promise<{ reasoningSettled: boolean } | void> {
1442
1457
  if (input.transcriptPath) this.lastTranscriptPath = input.transcriptPath;
1443
1458
 
1444
1459
  if (input.isPreFlush) {
1445
- if (!input.transcriptPath) return;
1446
- let jsonl: string | undefined;
1447
- try { jsonl = await readFile(input.transcriptPath, "utf8"); } catch { return; }
1448
- if (!jsonl) return;
1460
+ if (!input.transcriptPath) return { reasoningSettled: true };
1461
+ const settled = await this.settlePollForToolUse(input.transcriptPath, input.toolName, input.toolInput);
1462
+ const jsonl = settled.jsonl;
1463
+ if (jsonl === undefined) return { reasoningSettled: false };
1449
1464
  const body = extractLastAssistantTurnAfterEntry(jsonl, this.narrativeFlushEntryCount);
1450
1465
  await this.drainReasoningTail();
1451
1466
  this.narrativeFlushEntryCount = countTranscriptEntries(jsonl);
@@ -1455,7 +1470,7 @@ export class AgentRunner {
1455
1470
  await this.publishSessionEvent({ from: this.agentId, to: "user", body, session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) } });
1456
1471
  }
1457
1472
  await this.sessionOutbox.flush(2_000).then((ok) => { if (!ok) this.sessionLog(`pre-flush session outbox incomplete before blocking control (${this.sessionOutbox.pendingCount()} pending)`); }, (error) => this.sessionLog(`pre-flush session outbox failed before blocking control: ${errMessage(error)}`));
1458
- return;
1473
+ return { reasoningSettled: settled.settled };
1459
1474
  }
1460
1475
 
1461
1476
  // Claude's Stop hook reports its own native `prompt_id` — the same UUID the
@@ -1465,6 +1480,11 @@ export class AgentRunner {
1465
1480
  // prompt_id at turn start — see setProviderStatus / handleUserPrompt), but this
1466
1481
  // prefers the freshest source so finalization is correct even if the two drift.
1467
1482
  const turnId = input.promptId ?? this.currentTurnId;
1483
+ // #1116: drain whatever the tail hasn't polled yet BEFORE stopping it — stopping first
1484
+ // (the old order) clears the poll/timer state, so any steps Claude flushed in the last
1485
+ // window between the tail's previous tick and this Stop hook were silently dropped from
1486
+ // the step lane instead of reaching chat.
1487
+ await this.drainReasoningTail();
1468
1488
  this.stopReasoningTail();
1469
1489
  // Optional correlation for threading + obligation clearing — never a capture gate.
1470
1490
  let replyToMessageId: number | undefined, replyTarget = "user";
@@ -1550,22 +1570,18 @@ export class AgentRunner {
1550
1570
  // tool step) as a `kind: "session"` relay message tagged with payload.session so
1551
1571
  // the dashboard can render the live provider session faithfully. Display-only:
1552
1572
  // session messages are never delivered back into a provider.
1553
- private publishSessionEvent(input: {
1554
- from: string;
1555
- to: string;
1556
- body: string;
1557
- session: MessageSessionMeta;
1558
- replyTo?: number;
1559
- }): void {
1560
- // Durable, ordered, timestamped (#196): the actual POST happens in deliverOutboxEvent,
1561
- // retried until it lands. occurredAt is stamped now so a queued event reports when it
1562
- // truly happened, not when the server finally accepted it. Routed through the fast-lane
1563
- // sessionOutbox (#332) so a transient trace failure can't head-of-line block real messages.
1573
+ private publishSessionEvent(input: { from: string; to: string; body: string; session: MessageSessionMeta; replyTo?: number; occurredAt?: number }): void {
1574
+ // Durable, ordered, timestamped (#196/#1115): the actual POST happens in
1575
+ // deliverOutboxEvent, retried until it lands. An explicit occurredAt preserves the
1576
+ // provider-native event time; otherwise the outbox stamps enqueue time. Routed through
1577
+ // the fast-lane sessionOutbox (#332) so a transient trace failure can't head-of-line
1578
+ // block real messages.
1564
1579
  // A stepId-bearing step (Codex tool running→completed, streamed reasoning/response) uses a
1565
1580
  // STABLE idempotency key so the server upserts the row in place instead of appending a dup.
1566
1581
  const stepId = input.session.stepId;
1567
1582
  this.sessionOutbox.enqueue({
1568
1583
  kind: "session-message",
1584
+ ...(input.occurredAt ? { occurredAt: input.occurredAt } : {}),
1569
1585
  ...(stepId ? { idempotencyKey: `session-step:${input.from}:${input.session.turnId ?? ""}:${stepId}` } : {}),
1570
1586
  payload: {
1571
1587
  from: input.from,
@@ -1578,13 +1594,7 @@ export class AgentRunner {
1578
1594
  });
1579
1595
  }
1580
1596
 
1581
- private publishSessionEventsBatch(inputs: Array<{
1582
- from: string;
1583
- to: string;
1584
- body: string;
1585
- session: MessageSessionMeta;
1586
- replyTo?: number;
1587
- }>): void {
1597
+ private publishSessionEventsBatch(inputs: Array<{ from: string; to: string; body: string; session: MessageSessionMeta; replyTo?: number; occurredAt?: number }>): void {
1588
1598
  for (let i = 0; i < inputs.length; i += SESSION_MIRROR_BATCH_MAX) {
1589
1599
  const chunk = inputs.slice(i, i + SESSION_MIRROR_BATCH_MAX);
1590
1600
  if (chunk.length === 1) {
@@ -1599,6 +1609,7 @@ export class AgentRunner {
1599
1609
  ...(input.replyTo ? { replyTo: input.replyTo } : {}),
1600
1610
  kind: "session",
1601
1611
  body: input.body,
1612
+ ...(input.occurredAt ? { occurredAt: input.occurredAt } : {}),
1602
1613
  ...(stepId ? { idempotencyKey: `session-step:${input.from}:${input.session.turnId ?? ""}:${stepId}` } : {}),
1603
1614
  payload: { session: { provider: this.options.provider, ...input.session } },
1604
1615
  } satisfies SendMessageInput;
@@ -1940,7 +1951,7 @@ export class AgentRunner {
1940
1951
  }
1941
1952
  const turnId = this.currentTurnId ?? turnIdAtStart;
1942
1953
  let emitted = 0;
1943
- const batch: Array<{ from: string; to: string; body: string; session: MessageSessionMeta }> = [];
1954
+ const batch: Array<{ from: string; to: string; body: string; session: MessageSessionMeta; occurredAt?: number }> = [];
1944
1955
  for (const { sig, step } of keyed) {
1945
1956
  if (seen.has(sig)) continue;
1946
1957
  seen.add(sig);
@@ -1949,6 +1960,7 @@ export class AgentRunner {
1949
1960
  from: this.agentId,
1950
1961
  to: "user",
1951
1962
  body: step.text,
1963
+ ...(step.occurredAt ? { occurredAt: step.occurredAt } : {}),
1952
1964
  session: { type: step.type, origin: "provider", ...(turnId ? { turnId } : {}), ...(step.label ? { label: step.label } : {}) },
1953
1965
  });
1954
1966
  if (step.type === "narration") emittedNarrationKeys.add(sig);
@@ -1971,6 +1983,28 @@ export class AgentRunner {
1971
1983
  await this.reasoningTail?.poll();
1972
1984
  }
1973
1985
 
1986
+ // #1116: bounded settle-poll for the PreToolUse pre-flush anchor. The old pre-flush read
1987
+ // the transcript exactly once and, on the false premise that Claude's JSONL write is
1988
+ // synchronous with the hook firing, treated whatever it found (often nothing) as complete.
1989
+ // Claude's flush is actually async/bursty — sometimes gated on the hook's own resolution —
1990
+ // so this polls at settlePollIntervalMs until the transcript contains the EXACT tool_use
1991
+ // entry that triggered the hook (toolName/toolInput from the hook payload is the precise
1992
+ // anchor), then returns the freshest read. Never polls past settlePollTimeoutMs — the
1993
+ // question must ship regardless, just honestly flagged as `settled: false` when it does.
1994
+ // No toolName (a caller that can't identify an anchor) skips straight to a single read,
1995
+ // matching the old one-shot behavior.
1996
+ private async settlePollForToolUse(transcriptPath: string, toolName?: string, toolInput?: unknown): Promise<{ settled: boolean; jsonl?: string }> {
1997
+ const deadline = Date.now() + this.settlePollTimeoutMs;
1998
+ for (;;) {
1999
+ let jsonl: string | undefined;
2000
+ try { jsonl = await readFile(transcriptPath, "utf8"); } catch { jsonl = undefined; }
2001
+ if (!toolName) return { settled: Boolean(jsonl), jsonl };
2002
+ if (jsonl !== undefined && transcriptHasToolUseAnchor(jsonl, toolName, toolInput)) return { settled: true, jsonl };
2003
+ if (Date.now() >= deadline) return { settled: false, jsonl };
2004
+ await Bun.sleep(this.settlePollIntervalMs);
2005
+ }
2006
+ }
2007
+
1974
2008
  private preFlushBodyAlreadyMirroredByReasoningTail(jsonl: string, body: string): boolean {
1975
2009
  const tail = this.reasoningTail;
1976
2010
  if (!tail || !tail.emittedNarrationKeys.size) return false;
@@ -138,8 +138,10 @@ function normalizeMessagePayload(payload: MessagePayload | undefined): MessagePa
138
138
  * stable `payload.session.stepId`. Instead of appending a second row (which renders as a
139
139
  * duplicate on dashboard reload), update the EXISTING row in place: same DB id, so the
140
140
  * dashboard's id-keyed merge replaces it. Gated on stepId presence + matching step, so the
141
- * Claude transcript path (no stepId) is byte-identical. Newer-event-wins via occurredAt so a
142
- * late retry of an earlier lifecycle phase can't clobber a later one.
141
+ * Claude transcript path (no stepId) is byte-identical. The row keeps the first event's
142
+ * occurredAt so long-running tools stay anchored at their start; completion time rides in
143
+ * payload.session.completedAt. Lifecycle rank still prevents a late running retry from
144
+ * clobbering an already-completed row.
143
145
  */
144
146
  function upsertSessionStepInPlace(existing: Message, input: SendMessageInput, now: number): Message | null {
145
147
  const incomingStep = sessionStepId(input.payload);
@@ -147,14 +149,24 @@ function upsertSessionStepInPlace(existing: Message, input: SendMessageInput, no
147
149
  if (existing.kind !== "session") return null;
148
150
  if (sessionStepId(existing.payload) !== incomingStep) return null;
149
151
  const incomingAt = sanitizeOccurredAt(input.occurredAt, now) ?? now;
150
- const existingAt = existing.occurredAt ?? existing.createdAt;
151
- if (incomingAt < existingAt) return null; // stale lifecycle phase — keep the newer row, no re-emit
152
- getDb()
153
- .query("UPDATE messages SET body = ?, payload = ?, occurred_at = ? WHERE id = ?")
154
- .run(input.body, JSON.stringify(input.payload ?? {}), incomingAt, existing.id);
152
+ if (sessionStepStatusRank(input.payload) < sessionStepStatusRank(existing.payload)) return null;
153
+ const payload = sessionPayloadWithCompletion(input.payload, incomingAt);
154
+ getDb().query("UPDATE messages SET body = ?, payload = ? WHERE id = ?").run(input.body, JSON.stringify(payload), existing.id);
155
155
  return getMessage(existing.id);
156
156
  }
157
157
 
158
+ function sessionStepStatusRank(payload: MessagePayload | undefined): number {
159
+ const status = stringValue(isRecord(payload?.session) ? payload.session.status : undefined);
160
+ return status === "completed" || status === "failed" ? 2 : status === "running" ? 1 : 0;
161
+ }
162
+
163
+ function sessionPayloadWithCompletion(payload: MessagePayload | undefined, completedAt: number): MessagePayload {
164
+ const original = payload ?? {};
165
+ const session = isRecord(original.session) ? original.session : undefined;
166
+ const status = stringValue(session?.status);
167
+ return status === "completed" || status === "failed" ? { ...original, session: { ...session, completedAt } } : original;
168
+ }
169
+
158
170
  function policyNameFromTarget(target: string): string | null {
159
171
  if (!target.startsWith("policy:")) return null;
160
172
  const name = target.slice("policy:".length).trim();
@@ -256,7 +268,7 @@ function lineageStreamTurnId(message: Message, session: Record<string, unknown>)
256
268
  }
257
269
 
258
270
  function lineageStreamEventId(message: Message, session: Record<string, unknown>): string {
259
- const occurredAt = message.occurredAt ?? message.createdAt;
271
+ const occurredAt = typeof session.completedAt === "number" && Number.isFinite(session.completedAt) ? Math.floor(session.completedAt) : message.occurredAt ?? message.createdAt;
260
272
  const stepId = stringValue(session.stepId);
261
273
  return stepId ? `${stepId}:${occurredAt}` : `message:${occurredAt}`;
262
274
  }
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{B as t,Gn as n,H as r,Ht as i,Qt as a,Xn as o,_ as s,_t as c,a as l,et as u,g as d,gr as f,q as p,rr as m,rt as h,tr as g,u as _}from"./lucide-react-NXHPIMsX.js";import{p as ee,t as v}from"./store-D-Frk_EC.js";import{_ as te,h as y}from"./types-BqeSkWrv.js";import{J as b,K as x,W as S,Y as C,a as w,d as T,i as ne,n as re}from"./display-aTnwsqQh.js";import{o as E,r as D}from"./dist-BNB3fTO-.js";import{t as O}from"./dist-BD4Xc13v.js";import{i as ie,n as k}from"./dist-BFDfbfNC.js";import{t as A}from"./dist-DG75bvPp.js";import{t as j}from"./dist-CQMQ5iku.js";import{a as M,i as N,n as P,o as F,r as I,s as ae,t as oe}from"./es2015-a6fysEVz.js";import{a as L,i as se,n as R,r as z,t as B}from"./dist-C_VOBSaK.js";import{t as V}from"./badge-BCvp7LK3.js";import{t as H}from"./button-DyHjsHY3.js";import{t as ce}from"./copy-button-C67FUG3E.js";import{t as U}from"./agent-type-icon-BmBR-9CH.js";import{Dt as W,Et as le,J as ue,Ot as de,Y as fe,Z as pe,bt as me,ct as G,et as he,kt as ge,nt as _e,q as ve,rt as ye,tt as be}from"./index-BdNafiPz.js";import{r as xe,t as Se}from"./card-BoqYxpVW.js";import{i as Ce,n as we,r as Te,t as Ee}from"./branch-state-badge-Cscjqf-E.js";var K=e(f(),1),q=m(),J=`Popover`,[De,Oe]=ie(J,[L]),Y=L(),[ke,X]=De(J),Ae=e=>{let{__scopePopover:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!1}=e,s=Y(t),c=K.useRef(null),[l,u]=K.useState(!1),[d,f]=A({prop:r,defaultProp:i??!1,onChange:a,caller:J});return(0,q.jsx)(se,{...s,children:(0,q.jsx)(ke,{scope:t,contentId:ae(),triggerRef:c,open:d,onOpenChange:f,onOpenToggle:K.useCallback(()=>f(e=>!e),[f]),hasCustomAnchor:l,onCustomAnchorAdd:K.useCallback(()=>u(!0),[]),onCustomAnchorRemove:K.useCallback(()=>u(!1),[]),modal:o,children:n})})};Ae.displayName=J;var je=`PopoverAnchor`,Me=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(je,n),a=Y(n),{onCustomAnchorAdd:o,onCustomAnchorRemove:s}=i;return K.useEffect(()=>(o(),()=>s()),[o,s]),(0,q.jsx)(B,{...a,...r,ref:t})});Me.displayName=je;var Z=`PopoverTrigger`,Ne=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Z,n),a=Y(n),o=E(t,i.triggerRef),s=(0,q.jsx)(O.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":i.open,"aria-controls":i.contentId,"data-state":Ke(i.open),...r,ref:o,onClick:k(e.onClick,i.onOpenToggle)});return i.hasCustomAnchor?s:(0,q.jsx)(B,{asChild:!0,...a,children:s})});Ne.displayName=Z;var Q=`PopoverPortal`,[Pe,Fe]=De(Q,{forceMount:void 0}),Ie=e=>{let{__scopePopover:t,forceMount:n,children:r,container:i}=e,a=X(Q,t);return(0,q.jsx)(Pe,{scope:t,forceMount:n,children:(0,q.jsx)(j,{present:n||a.open,children:(0,q.jsx)(N,{asChild:!0,container:i,children:r})})})};Ie.displayName=Q;var $=`PopoverContent`,Le=K.forwardRef((e,t)=>{let n=Fe($,e.__scopePopover),{forceMount:r=n.forceMount,...i}=e,a=X($,e.__scopePopover);return(0,q.jsx)(j,{present:r||a.open,children:a.modal?(0,q.jsx)(ze,{...i,ref:t}):(0,q.jsx)(Be,{...i,ref:t})})});Le.displayName=$;var Re=D(`PopoverContent.RemoveScroll`),ze=K.forwardRef((e,t)=>{let n=X($,e.__scopePopover),r=K.useRef(null),i=E(t,r),a=K.useRef(!1);return K.useEffect(()=>{let e=r.current;if(e)return oe(e)},[]),(0,q.jsx)(P,{as:Re,allowPinchZoom:!0,children:(0,q.jsx)(Ve,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:k(e.onCloseAutoFocus,e=>{e.preventDefault(),a.current||n.triggerRef.current?.focus()}),onPointerDownOutside:k(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;a.current=t.button===2||n},{checkForDefaultPrevented:!1}),onFocusOutside:k(e.onFocusOutside,e=>e.preventDefault(),{checkForDefaultPrevented:!1})})})}),Be=K.forwardRef((e,t)=>{let n=X($,e.__scopePopover),r=K.useRef(!1),i=K.useRef(!1);return(0,q.jsx)(Ve,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),Ve=K.forwardRef((e,t)=>{let{__scopePopover:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:a,disableOutsidePointerEvents:o,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onInteractOutside:u,...d}=e,f=X($,n),p=Y(n);return I(),(0,q.jsx)(M,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:a,children:(0,q.jsx)(F,{asChild:!0,disableOutsidePointerEvents:o,onInteractOutside:u,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onDismiss:()=>f.onOpenChange(!1),children:(0,q.jsx)(z,{"data-state":Ke(f.open),role:`dialog`,id:f.contentId,...p,...d,ref:t,style:{...d.style,"--radix-popover-content-transform-origin":`var(--radix-popper-transform-origin)`,"--radix-popover-content-available-width":`var(--radix-popper-available-width)`,"--radix-popover-content-available-height":`var(--radix-popper-available-height)`,"--radix-popover-trigger-width":`var(--radix-popper-anchor-width)`,"--radix-popover-trigger-height":`var(--radix-popper-anchor-height)`}})})})}),He=`PopoverClose`,Ue=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(He,n);return(0,q.jsx)(O.button,{type:`button`,...r,ref:t,onClick:k(e.onClick,()=>i.onOpenChange(!1))})});Ue.displayName=He;var We=`PopoverArrow`,Ge=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=Y(n);return(0,q.jsx)(R,{...i,...r,ref:t})});Ge.displayName=We;function Ke(e){return e?`open`:`closed`}var qe=Ae,Je=Ne,Ye=Ie,Xe=Le;function Ze({...e}){return(0,q.jsx)(qe,{"data-slot":`popover`,...e})}function Qe({...e}){return(0,q.jsx)(Je,{"data-slot":`popover-trigger`,...e})}function $e({className:e,align:t=`center`,sideOffset:n=4,...r}){return(0,q.jsx)(Ye,{children:(0,q.jsx)(Xe,{"data-slot":`popover-content`,align:t,sideOffset:n,className:g(`z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r})})}function et({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-header`,className:g(`flex flex-col gap-0.5 text-sm`,e),...t})}function tt({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-title`,className:g(`font-medium`,e),...t})}function nt({agent:e,childAgents:n=[],parentAgent:r}){let o=ee();v(e=>e.set);let l=v(e=>e.switchView),f=v(e=>e.openInboxThread),m=v(e=>e.openAgentDetail),g=v(e=>e.openPairInvite),te=v(e=>e.openRename),y=v(e=>e.openConfirm),E=v(e=>e.doAgentAction),D=v(e=>e.doDeleteAgent),O=v(e=>e.openFilesForAgent),ie=v(e=>e.workspaceAction),k=v(e=>e.openWorkspaceFocus),{terminalOpen:A,terminalTarget:j,terminalOpening:M,openTerminal:N,closeTerminal:P}=Ce(e.id),F=le(),I=ne(o,e,me(e),F[e.id]||null),ae=w(e,`shutdown`),oe=w(e,`compact`),L=w(e,`clearContext`),se=!!(e.providerCapabilities?.terminal?.live?.read||e.providerCapabilities?.terminal?.attach?.create),R=!!e.providerCapabilities?.terminal?.live?.write,z=re(e),B=r?T(r):e.spawnedBy,W=n.length>0;return(0,q.jsxs)(q.Fragment,{children:[(0,q.jsx)(Se,{className:`group hover:border-zinc-600 transition-colors cursor-pointer`,onClick:()=>m(e),children:(0,q.jsxs)(xe,{className:`p-4`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2.5 mb-2.5`,children:[(0,q.jsx)(G,{agent:e,now:o,className:`mt-0.5`}),(0,q.jsx)(U,{agent:e}),(0,q.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,q.jsxs)(`div`,{className:`flex items-center gap-1 min-w-0`,children:[(0,q.jsx)(we,{profile:Te(e),className:`text-sm shrink-0`}),(0,q.jsx)(`span`,{className:`text-sm font-medium truncate`,children:T(e)})]}),(0,q.jsx)(`div`,{className:`text-xs mt-0.5 ${b(I.tone)}`,children:I.label}),typeof e.meta?.cwd==`string`&&e.meta.cwd&&(0,q.jsx)(`button`,{type:`button`,className:`mt-0.5 block max-w-full truncate font-mono text-[10px] text-muted-foreground hover:text-foreground`,title:`Open ${e.meta.cwd} in Files`,onClick:t=>{t.stopPropagation(),O(e)},children:S(e.meta.cwd)}),(0,q.jsx)(pe,{agent:e,className:`mt-1`})]}),e.branchState&&e.branchWorkspaceId&&(0,q.jsx)(Ee,{state:e.branchState,className:`mt-0.5 shrink-0`,onClick:t=>{t.stopPropagation(),k(e.branchWorkspaceId)}})]}),I.badges.length>0&&(0,q.jsx)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:I.badges.map(e=>(0,q.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded-full font-medium ${e.className}`,children:e.label},e.label))}),(W||e.spawnedBy&&B)&&(0,q.jsxs)(`div`,{className:`mb-2 flex flex-wrap gap-1.5`,onClick:e=>e.stopPropagation(),children:[W&&(0,q.jsxs)(Ze,{children:[(0,q.jsx)(Qe,{asChild:!0,children:(0,q.jsxs)(`button`,{type:`button`,className:`inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border border-sky-500/30 bg-sky-500/10 px-2 text-[10px] font-medium text-sky-300 transition-colors hover:bg-sky-500/15`,title:`${n.length} live child ${n.length===1?`agent`:`agents`}`,children:[(0,q.jsx)(_,{className:`h-3 w-3`}),(0,q.jsx)(`span`,{children:n.length}),(0,q.jsx)(`span`,{className:`flex -space-x-1`,children:n.slice(0,4).map(e=>(0,q.jsx)(G,{agent:e,now:o,className:`h-2 w-2 ring-1 ring-background`},e.id))})]})}),(0,q.jsxs)($e,{align:`start`,className:`w-80`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(et,{children:(0,q.jsx)(tt,{children:`Team`})}),(0,q.jsx)(`div`,{className:`space-y-1.5`,children:n.map(e=>(0,q.jsxs)(`div`,{className:`rounded-md border border-border bg-card/60 p-2`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,q.jsx)(G,{agent:e,now:o,className:`mt-1`}),(0,q.jsx)(U,{agent:e}),(0,q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,q.jsx)(`div`,{className:`truncate text-xs font-medium`,children:T(e)}),(0,q.jsx)(`div`,{className:`truncate text-[10px] text-muted-foreground`,children:e.providerCapabilities?.model?.alias||e.providerCapabilities?.model?.id||e.providerCapabilities?.model?.provider||e.status})]}),(0,q.jsx)(V,{variant:`outline`,className:`h-4 px-1.5 text-[9px]`,children:e.status})]}),(0,q.jsxs)(`div`,{className:`mt-2 flex gap-1`,children:[(0,q.jsx)(H,{size:`xs`,variant:`outline`,onClick:()=>m(e),children:`Open`}),(0,q.jsxs)(H,{size:`xs`,variant:`ghost`,onClick:()=>{f(e.id),l(`chat`)},children:[(0,q.jsx)(h,{className:`h-3 w-3`}),`Chat`]})]})]},e.id))})]})]}),e.spawnedBy&&B&&(0,q.jsx)(V,{variant:`outline`,className:`max-w-full text-[10px] text-muted-foreground`,children:(0,q.jsxs)(`span`,{className:`truncate`,children:[`spawned by `,B]})})]}),(e.tags.length>0||C(e.capabilities).length>0)&&(0,q.jsxs)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:[e.tags.slice(0,3).map(e=>(0,q.jsx)(V,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e)),C(e.capabilities).slice(0,2).map(e=>(0,q.jsx)(V,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))]}),(0,q.jsxs)(`div`,{className:`space-y-2 mb-2`,children:[(0,q.jsx)(_e,{agent:e}),(0,q.jsx)(be,{agent:e,compact:!0}),(0,q.jsx)(ye,{agent:e})]}),(0,q.jsx)(`div`,{className:`text-xs text-muted-foreground`,children:e.id!==`user`&&e.id!==`system`&&x(o,e.lastSeen)}),(0,q.jsxs)(`div`,{className:`flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(ce,{value:e.id,label:`Copy agent ID`,size:`icon`,className:`h-7 w-7`,iconClassName:`w-3 h-3`}),(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Chat`,onClick:()=>{f(e.id),l(`chat`)},children:(0,q.jsx)(h,{className:`w-3 h-3`})}),(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Pair`,onClick:()=>g(e.id),children:(0,q.jsx)(c,{className:`w-3 h-3`})}),(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Rename`,onClick:()=>te(e),children:(0,q.jsx)(p,{className:`w-3 h-3`})}),se&&(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Terminal`,disabled:M,onClick:()=>void N(),children:(0,q.jsx)(s,{className:`w-3 h-3`})}),(e.branchState===`changes`||e.branchState===`idle`)&&e.branchWorkspaceId&&(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-amber-400 hover:text-amber-300`,title:`Mark workspace ready — hand off to the auto-merge`,onClick:()=>y(`Mark Workspace Ready`,`Mark ${T(e)}'s branch ready to land? The relay auto-merge will rebase and land it.`,()=>ie(e.branchWorkspaceId,`ready`)),children:(0,q.jsx)(i,{className:`w-3 h-3`})}),oe&&(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Compact`,onClick:()=>y(`Compact Agent`,`Compact context for ${T(e)}?`,()=>E(e,`compact`,`agents-grid`)),children:(0,q.jsx)(u,{className:`w-3 h-3`})}),L&&(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Clear Context`,onClick:()=>y(`Clear Context`,`Clear context for ${T(e)}?`,()=>E(e,`clearContext`,`agents-grid`)),children:(0,q.jsx)(a,{className:`w-3 h-3`})}),ae&&(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Shutdown`,onClick:()=>y(`Shutdown Agent`,`Shutdown ${T(e)}?`,()=>E(e,`shutdown`,`agents-grid`)),children:(0,q.jsx)(t,{className:`w-3 h-3`})}),z&&(0,q.jsx)(H,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Forget stale agent`,onClick:()=>y(`Forget Agent`,`Forget ${T(e)}?`,()=>D(e.id)),children:(0,q.jsx)(d,{className:`w-3 h-3`})})]})]})}),j&&(0,q.jsx)(he,{open:A,onOpenChange:P,orchestratorId:j.orchestratorId,session:j.session,interactive:j.interactive||R,canWrite:R})]})}function rt(){let e=v(e=>e.agentPresetFilter),t=v(e=>e.agentStatusFilter),i=v(e=>e.agentTagFilter),a=v(e=>e.agentHostFilter),s=v(e=>e.agentSort),c=v(e=>e.agentSortDir),u=v(e=>e.showOffline),d=v(e=>e.set),f=v(e=>e.openAgentSpawn),p=v(e=>e.agentsById),m=ee(),h=W(),g=ge(),_=de(),b=ve(h,m),x=fe(h,m),S=e||t||i||a;function C(){d({agentPresetFilter:``,agentStatusFilter:``,agentTagFilter:``,agentHostFilter:``})}function w(){d({agentSortDir:c===`asc`?`desc`:`asc`})}return(0,q.jsxs)(`div`,{className:`space-y-4`,children:[(0,q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,q.jsxs)(`select`,{value:e,onChange:e=>d({agentPresetFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`All agents`}),(0,q.jsx)(`option`,{value:`active`,children:`Active`}),y().map(e=>(0,q.jsx)(`option`,{value:e,children:te(e)},e)),(0,q.jsx)(`option`,{value:`paired`,children:`Paired`}),(0,q.jsx)(`option`,{value:`unpaired`,children:`Unpaired`}),(0,q.jsx)(`option`,{value:`offline_stale`,children:`Offline / Stale`})]}),(0,q.jsxs)(`select`,{value:t,onChange:e=>d({agentStatusFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any status`}),(0,q.jsx)(`option`,{value:`online`,children:`Online`}),(0,q.jsx)(`option`,{value:`idle`,children:`Idle`}),(0,q.jsx)(`option`,{value:`busy`,children:`Busy`}),(0,q.jsx)(`option`,{value:`offline`,children:`Offline`}),(0,q.jsx)(`option`,{value:`starting`,children:`Starting`})]}),g.length>0&&(0,q.jsxs)(`select`,{value:i,onChange:e=>d({agentTagFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any tag`}),g.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),_.length>1&&(0,q.jsxs)(`select`,{value:a,onChange:e=>d({agentHostFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any host`}),_.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),(0,q.jsxs)(`select`,{value:s,onChange:e=>d({agentSort:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:`status`,children:`Sort: Status`}),(0,q.jsx)(`option`,{value:`name`,children:`Sort: Name`}),(0,q.jsx)(`option`,{value:`lastSeen`,children:`Sort: Last seen`}),(0,q.jsx)(`option`,{value:`created`,children:`Sort: Created`})]}),(0,q.jsx)(H,{size:`icon`,variant:`outline`,className:`h-8 w-8`,title:`Toggle sort direction`,onClick:w,children:c===`asc`?(0,q.jsx)(n,{className:`w-3.5 h-3.5`}):(0,q.jsx)(o,{className:`w-3.5 h-3.5`})}),(0,q.jsxs)(`label`,{className:`flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer select-none`,children:[(0,q.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d({showOffline:e.target.checked}),className:`rounded`}),`Show offline`]}),S&&(0,q.jsxs)(H,{size:`sm`,variant:`ghost`,className:`h-8 text-muted-foreground hover:text-foreground gap-1`,onClick:C,children:[(0,q.jsx)(l,{className:`w-3 h-3`}),`Clear filters`]}),(0,q.jsx)(`div`,{className:`ml-auto`,children:(0,q.jsxs)(H,{size:`sm`,onClick:f,className:`gap-1`,children:[(0,q.jsx)(r,{className:`w-3.5 h-3.5`}),`Spawn`]})})]}),x.length===0?(0,q.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No agents match the current filters.`}):(0,q.jsx)(`div`,{className:`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3`,children:x.map(e=>(0,q.jsx)(nt,{agent:e,parentAgent:e.spawnedBy?p[e.spawnedBy]??null:null,childAgents:ue(b.get(e.id)??[])},e.id))})]})}export{rt as AgentsView};
2
- //# sourceMappingURL=agents-Dt5TncjC.js.map