@namzu/cli 16.0.0 → 17.0.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.
Files changed (91) hide show
  1. package/README.md +34 -6
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +43 -8
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/drain.d.ts.map +1 -1
  6. package/dist/commands/drain.js +23 -1
  7. package/dist/commands/drain.js.map +1 -1
  8. package/dist/commands/run-stream.d.ts +3 -2
  9. package/dist/commands/run-stream.d.ts.map +1 -1
  10. package/dist/commands/run-stream.js +32 -37
  11. package/dist/commands/run-stream.js.map +1 -1
  12. package/dist/commands/run.d.ts.map +1 -1
  13. package/dist/commands/run.js +26 -24
  14. package/dist/commands/run.js.map +1 -1
  15. package/dist/commands/state.d.ts +9 -0
  16. package/dist/commands/state.d.ts.map +1 -0
  17. package/dist/commands/state.js +110 -0
  18. package/dist/commands/state.js.map +1 -0
  19. package/dist/config/load.d.ts +1 -1
  20. package/dist/config/load.d.ts.map +1 -1
  21. package/dist/config/load.js +3 -4
  22. package/dist/config/load.js.map +1 -1
  23. package/dist/doctor/checks/chain.d.ts +1 -1
  24. package/dist/doctor/checks/chain.d.ts.map +1 -1
  25. package/dist/doctor/checks/chain.js +2 -4
  26. package/dist/doctor/checks/chain.js.map +1 -1
  27. package/dist/integrations/files/attachment-store.d.ts.map +1 -1
  28. package/dist/integrations/files/attachment-store.js +3 -3
  29. package/dist/integrations/files/attachment-store.js.map +1 -1
  30. package/dist/integrations/plugins/runtime.d.ts.map +1 -1
  31. package/dist/integrations/plugins/runtime.js +4 -2
  32. package/dist/integrations/plugins/runtime.js.map +1 -1
  33. package/dist/integrations/providers/credential-store.d.ts.map +1 -1
  34. package/dist/integrations/providers/credential-store.js +13 -12
  35. package/dist/integrations/providers/credential-store.js.map +1 -1
  36. package/dist/integrations/providers/preferences.d.ts.map +1 -1
  37. package/dist/integrations/providers/preferences.js +5 -5
  38. package/dist/integrations/providers/preferences.js.map +1 -1
  39. package/dist/integrations/sessions/store.d.ts +31 -9
  40. package/dist/integrations/sessions/store.d.ts.map +1 -1
  41. package/dist/integrations/sessions/store.js +308 -63
  42. package/dist/integrations/sessions/store.js.map +1 -1
  43. package/dist/integrations/state/home.d.ts +25 -0
  44. package/dist/integrations/state/home.d.ts.map +1 -0
  45. package/dist/integrations/state/home.js +53 -0
  46. package/dist/integrations/state/home.js.map +1 -0
  47. package/dist/integrations/state/private-directory.d.ts +15 -0
  48. package/dist/integrations/state/private-directory.d.ts.map +1 -0
  49. package/dist/integrations/state/private-directory.js +41 -0
  50. package/dist/integrations/state/private-directory.js.map +1 -0
  51. package/dist/integrations/state/report.d.ts +129 -0
  52. package/dist/integrations/state/report.d.ts.map +1 -0
  53. package/dist/integrations/state/report.js +831 -0
  54. package/dist/integrations/state/report.js.map +1 -0
  55. package/dist/integrations/subagents/activity.d.ts +18 -0
  56. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  57. package/dist/integrations/subagents/activity.js +64 -7
  58. package/dist/integrations/subagents/activity.js.map +1 -1
  59. package/dist/integrations/subagents/runtime.d.ts +3 -1
  60. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  61. package/dist/integrations/subagents/runtime.js +46 -7
  62. package/dist/integrations/subagents/runtime.js.map +1 -1
  63. package/dist/integrations/trust/store.d.ts.map +1 -1
  64. package/dist/integrations/trust/store.js +6 -6
  65. package/dist/integrations/trust/store.js.map +1 -1
  66. package/dist/memory/store.d.ts.map +1 -1
  67. package/dist/memory/store.js +7 -7
  68. package/dist/memory/store.js.map +1 -1
  69. package/dist/skills/store.d.ts +2 -1
  70. package/dist/skills/store.d.ts.map +1 -1
  71. package/dist/skills/store.js +11 -4
  72. package/dist/skills/store.js.map +1 -1
  73. package/dist/tui/AgentExplorer.d.ts +26 -5
  74. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  75. package/dist/tui/AgentExplorer.js +125 -14
  76. package/dist/tui/AgentExplorer.js.map +1 -1
  77. package/dist/tui/App.d.ts.map +1 -1
  78. package/dist/tui/App.js +263 -96
  79. package/dist/tui/App.js.map +1 -1
  80. package/dist/tui/agent.d.ts +10 -2
  81. package/dist/tui/agent.d.ts.map +1 -1
  82. package/dist/tui/agent.js +87 -21
  83. package/dist/tui/agent.js.map +1 -1
  84. package/dist/tui/slashCommands.d.ts +1 -1
  85. package/dist/tui/slashCommands.d.ts.map +1 -1
  86. package/dist/tui/slashCommands.js +2 -2
  87. package/dist/tui/slashCommands.js.map +1 -1
  88. package/dist/user-commands/store.d.ts.map +1 -1
  89. package/dist/user-commands/store.js +29 -7
  90. package/dist/user-commands/store.js.map +1 -1
  91. package/package.json +2 -2
package/dist/tui/App.js CHANGED
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
12
12
  * automatic reuse is not mistaken for a durable operator preference.
13
13
  */
14
14
  import { join, relative } from 'node:path';
15
- import { DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
15
+ import { DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
16
16
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
17
17
  import { useCallback, useEffect, useRef, useState } from 'react';
18
18
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
@@ -28,7 +28,7 @@ import { checkUpdates } from '../integrations/updates.js';
28
28
  import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
29
29
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
30
30
  import { discoverUserCommands } from '../user-commands/store.js';
31
- import { AgentPicker, AgentTranscript, agentPickerPageSize, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
31
+ import { AgentCockpit, AgentTranscript, agentCockpitIsWide, agentPickerPageSize, agentPhasePageSize, agentPhases, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
32
32
  import { ChoicePicker } from './ChoicePicker.js';
33
33
  import { Composer, suggestionWindowSize, } from './Composer.js';
34
34
  import { CopyPicker } from './CopyPicker.js';
@@ -462,9 +462,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
462
462
  subagentsRef.current = next;
463
463
  setSubagentsState(next);
464
464
  const surface = agentSurfaceRef.current;
465
- if (surface && !next.some((agent) => agent.viewId === surface.selectedId)) {
465
+ if (!surface || next.some((agent) => agent.viewId === surface.selectedId))
466
+ return;
467
+ const phases = agentPhases(next);
468
+ const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? phases[0];
469
+ const fallbackAgent = fallbackPhase?.agents[0];
470
+ if (!fallbackPhase || !fallbackAgent) {
466
471
  setAgentSurface(null);
472
+ return;
467
473
  }
474
+ setAgentSurface({
475
+ kind: 'cockpit',
476
+ selectedPhaseId: fallbackPhase.id,
477
+ selectedId: fallbackAgent.viewId,
478
+ focus: surface.kind === 'cockpit' ? surface.focus : surface.returnFocus,
479
+ });
468
480
  }, [setAgentSurface]);
469
481
  /** One git-backed review choice may be resolving while its visible picker remains authoritative. */
470
482
  const reviewChoiceInFlightRef = useRef(null);
@@ -551,9 +563,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
551
563
  // turns attribute to the resumed conversation.
552
564
  const sessionsRef = useRef(null);
553
565
  const scopeRef = useRef(null);
566
+ /**
567
+ * A provider session can be ready before a person has started a conversation.
568
+ * The scope still needs a mutable session cursor, but its generated id is
569
+ * provisional until the first admitted prompt or an explicit conversation
570
+ * operation publishes the durable Session.
571
+ */
572
+ const conversationMaterializedRef = useRef(false);
573
+ const materializationRef = useRef(null);
554
574
  const exitWithSummary = useCallback(() => {
555
575
  onExitSummary?.({
556
- ...(scopeRef.current?.sessionId ? { conversationId: scopeRef.current.sessionId } : {}),
576
+ ...(conversationMaterializedRef.current && scopeRef.current?.sessionId
577
+ ? { conversationId: scopeRef.current.sessionId }
578
+ : {}),
557
579
  });
558
580
  exit();
559
581
  }, [exit, onExitSummary]);
@@ -830,12 +852,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
830
852
  }
831
853
  applyReasoningEffort(next);
832
854
  }, [applyReasoningEffort, pushMessage, session]);
833
- const recordFeedback = useCallback((runIdValue, messageIdValue, rating, note) => {
834
- // Written under the same `<cwd>/.namzu` root the runs live in, so a
835
- // rating and the transcript it judges travel together.
855
+ const recordFeedback = useCallback((sessionId, runIdValue, messageIdValue, rating, note) => {
856
+ const sessions = sessionsRef.current;
857
+ if (!sessions) {
858
+ pushMessage('system', 'Could not record feedback: conversation persistence is unavailable.');
859
+ return;
860
+ }
861
+ // Run evidence is session-scoped in the canonical store. The old flat
862
+ // `<cwd>/.namzu/runs` lookup never existed after the hierarchy migration,
863
+ // so the feedback store correctly refused every TUI rating as unverifiable.
864
+ // Bind both trees to the exact conversation captured with the streamed
865
+ // message; `/resume` may change the active scope before a delayed click.
866
+ const sessionDir = new DefaultPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
836
867
  const store = new DiskMessageFeedbackStore({
837
- rootDir: join(ctx.cwd, '.namzu', 'feedback'),
838
- runsDir: join(ctx.cwd, '.namzu', 'runs'),
868
+ rootDir: join(sessionDir, 'feedback'),
869
+ runsDir: join(sessionDir, 'runs'),
839
870
  });
840
871
  const runId = runIdValue;
841
872
  const messageId = messageIdValue;
@@ -857,7 +888,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
857
888
  pushMessage('system', `Could not record feedback: ${error instanceof Error ? error.message : String(error)}`);
858
889
  }
859
890
  })();
860
- }, [ctx.cwd, pushMessage]);
891
+ }, [pushMessage]);
861
892
  const activateSkill = useCallback((name) => {
862
893
  const info = discoverSkills({ cwd: ctx.cwd }).find((skill) => skill.name === name);
863
894
  if (!info) {
@@ -904,8 +935,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
904
935
  return;
905
936
  const sessions = sessionsRef.current;
906
937
  const sessionId = scopeRef.current?.sessionId;
907
- if (!sessions || !sessionId) {
908
- pushMessage('system', 'Cannot archive this conversation because durable session persistence is unavailable.');
938
+ if (!sessions || !sessionId || !conversationMaterializedRef.current) {
939
+ pushMessage('system', !conversationMaterializedRef.current
940
+ ? 'There is no conversation to archive yet — send a message first.'
941
+ : 'Cannot archive this conversation because durable session persistence is unavailable.');
909
942
  return;
910
943
  }
911
944
  if (state !== 'idle' ||
@@ -956,8 +989,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
956
989
  const stableExportSource = useCallback(() => {
957
990
  const sessions = sessionsRef.current;
958
991
  const sessionId = scopeRef.current?.sessionId;
959
- if (!sessions || !sessionId) {
960
- pushMessage('system', 'Cannot export this conversation because durable session persistence is unavailable.');
992
+ if (!sessions || !sessionId || !conversationMaterializedRef.current) {
993
+ pushMessage('system', !conversationMaterializedRef.current
994
+ ? 'There is no conversation to export yet — send a message first.'
995
+ : 'Cannot export this conversation because durable session persistence is unavailable.');
961
996
  return null;
962
997
  }
963
998
  if (abortRef.current ||
@@ -1204,7 +1239,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1204
1239
  return;
1205
1240
  }
1206
1241
  if (picker.kind === 'feedback-rating') {
1207
- recordFeedback(picker.runId, picker.messageId, value);
1242
+ recordFeedback(picker.sessionId, picker.runId, picker.messageId, value);
1208
1243
  return;
1209
1244
  }
1210
1245
  if (picker.kind === 'skill') {
@@ -1303,45 +1338,81 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1303
1338
  setMessages((prev) => prev.map((m) => m.id === id ? { ...m, content: finalContent ?? m.content, pending: false } : m));
1304
1339
  }, []);
1305
1340
  // Open the SDK session store and select the durable conversation once.
1306
- // Ordinary startup remains best-effort; an explicit `namzu resume <id>` is
1307
- // exact and therefore refuses instead of silently widening to a fresh chat.
1341
+ // This is an admission gate for every startup, not optional persistence: a
1342
+ // corrupt or split estate must not silently widen into cwd-local state and
1343
+ // let the model run against a different history than the operator selected.
1308
1344
  const ensureSessions = useCallback(async () => {
1309
1345
  if (scopeRef.current)
1310
1346
  return scopeRef.current;
1311
1347
  const requestedConversationId = initialConversationIdRef.current;
1312
- try {
1313
- const sessions = await openSessions(ctxRef.current.cwd);
1314
- let sessionId;
1315
- if (requestedConversationId) {
1316
- const restored = await loadResumableConversation(sessions, requestedConversationId);
1317
- sessionId = asSessionId(requestedConversationId);
1318
- modelHistoryRef.current = restored;
1319
- setMessages(projectConversation(restored, nextId));
1320
- setHistory(promptHistoryFromConversation(restored));
1321
- const persistedOutput = latestAssistantOutput(restored);
1322
- lastCompletedOutputRef.current = persistedOutput
1323
- ? { text: persistedOutput, provenance: 'persisted' }
1324
- : null;
1325
- initialConversationIdRef.current = undefined;
1348
+ const sessions = await openSessions(ctxRef.current.cwd);
1349
+ let sessionId;
1350
+ if (requestedConversationId) {
1351
+ const restored = await loadResumableConversation(sessions, requestedConversationId);
1352
+ sessionId = asSessionId(requestedConversationId);
1353
+ modelHistoryRef.current = restored;
1354
+ setMessages(projectConversation(restored, nextId));
1355
+ setHistory(promptHistoryFromConversation(restored));
1356
+ const persistedOutput = latestAssistantOutput(restored);
1357
+ lastCompletedOutputRef.current = persistedOutput
1358
+ ? { text: persistedOutput, provenance: 'persisted' }
1359
+ : null;
1360
+ initialConversationIdRef.current = undefined;
1361
+ conversationMaterializedRef.current = true;
1362
+ }
1363
+ else {
1364
+ // Provider/tool startup needs stable project/tenant/topic ids, not a
1365
+ // durable conversation. The cursor is replaced on first admitted use.
1366
+ sessionId = generateSessionId();
1367
+ }
1368
+ sessionsRef.current = sessions;
1369
+ scopeRef.current = {
1370
+ sessionId,
1371
+ topicId: sessions.topicId,
1372
+ projectId: sessions.projectId,
1373
+ tenantId: sessions.tenantId,
1374
+ };
1375
+ return scopeRef.current;
1376
+ }, [nextId]);
1377
+ /** Publish the provisional conversation exactly once, at first durable use. */
1378
+ const materializeConversation = useCallback(async () => {
1379
+ const existing = await ensureSessions();
1380
+ if (!existing)
1381
+ return undefined;
1382
+ if (conversationMaterializedRef.current)
1383
+ return existing;
1384
+ if (materializationRef.current)
1385
+ return materializationRef.current;
1386
+ const pending = (async () => {
1387
+ const sessions = sessionsRef.current;
1388
+ const scope = scopeRef.current;
1389
+ if (!sessions || !scope)
1390
+ return undefined;
1391
+ conversationMutationRef.current = 'materialize';
1392
+ try {
1393
+ const sessionId = await startConversation(sessions);
1394
+ if (scopeRef.current !== scope || conversationMaterializedRef.current) {
1395
+ throw new Error('the active conversation changed while its first session was published');
1396
+ }
1397
+ scope.sessionId = sessionId;
1398
+ conversationMaterializedRef.current = true;
1399
+ return scope;
1326
1400
  }
1327
- else {
1328
- sessionId = await startConversation(sessions);
1401
+ finally {
1402
+ if (conversationMutationRef.current === 'materialize') {
1403
+ conversationMutationRef.current = null;
1404
+ }
1329
1405
  }
1330
- sessionsRef.current = sessions;
1331
- scopeRef.current = {
1332
- sessionId,
1333
- topicId: sessions.topicId,
1334
- projectId: sessions.projectId,
1335
- tenantId: sessions.tenantId,
1336
- };
1337
- return scopeRef.current;
1406
+ })();
1407
+ materializationRef.current = pending;
1408
+ try {
1409
+ return await pending;
1338
1410
  }
1339
- catch (error) {
1340
- if (requestedConversationId)
1341
- throw error;
1342
- return undefined;
1411
+ finally {
1412
+ if (materializationRef.current === pending)
1413
+ materializationRef.current = null;
1343
1414
  }
1344
- }, [nextId]);
1415
+ }, [ensureSessions]);
1345
1416
  const hydrateSession = useCallback(async (prefs, detectedNow, signal) => {
1346
1417
  if (signal?.aborted)
1347
1418
  return;
@@ -1352,6 +1423,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1352
1423
  const s = await createAgentSession(prefs, detectedNow, {
1353
1424
  scope,
1354
1425
  cwd: activeCtx.cwd,
1426
+ ...(sessionsRef.current?.backend === 'central'
1427
+ ? { stateRoot: sessionsRef.current.root }
1428
+ : {}),
1355
1429
  enableComputerUse: true,
1356
1430
  rules: activeCtx.rules,
1357
1431
  ...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
@@ -1985,11 +2059,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1985
2059
  setMessages(restored);
1986
2060
  modelHistoryRef.current = msgs;
1987
2061
  setHistory((previous) => [...previous, ...promptHistoryFromConversation(msgs)]);
2062
+ // Persisted conversation rows do not carry the live run/message identity
2063
+ // required by feedback validation. Do not leave the answer from the
2064
+ // conversation we just exited as a hidden `/feedback` target.
2065
+ lastAssistantMessage.current = null;
1988
2066
  const persistedOutput = latestAssistantOutput(msgs);
1989
2067
  lastCompletedOutputRef.current = persistedOutput
1990
2068
  ? { text: persistedOutput, provenance: 'persisted' }
1991
2069
  : null;
1992
2070
  scope.sessionId = conv.id; // new turns now attribute to the resumed session
2071
+ conversationMaterializedRef.current = true;
1993
2072
  pushMessage('system', `Resumed: ${conv.title}`);
1994
2073
  if (discardedQueued > 0) {
1995
2074
  pushMessage('system', `Discarded ${discardedQueued} queued prompt${discardedQueued === 1 ? '' : 's'} from the conversation you left.`);
@@ -2053,8 +2132,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2053
2132
  modelHistoryRef.current = [];
2054
2133
  lastAssistantMessage.current = null;
2055
2134
  lastCompletedOutputRef.current = null;
2056
- if (sourceScope && targetSessionId)
2135
+ if (sourceScope && targetSessionId) {
2057
2136
  sourceScope.sessionId = targetSessionId;
2137
+ conversationMaterializedRef.current = true;
2138
+ }
2058
2139
  if (clearScreen) {
2059
2140
  setMessages([]);
2060
2141
  resetTranscript();
@@ -2098,8 +2179,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2098
2179
  * history; it is a session-store operation with the session id captured here.
2099
2180
  */
2100
2181
  const doTitle = useCallback(async (title, clear) => {
2101
- const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2102
- const scope = scopeRef.current;
2182
+ const scope = await materializeConversation();
2183
+ const sessions = sessionsRef.current;
2103
2184
  if (!sessions || !scope) {
2104
2185
  pushMessage('system', 'Conversation history is unavailable in this folder.');
2105
2186
  return;
@@ -2127,7 +2208,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2127
2208
  catch (err) {
2128
2209
  pushMessage('system', `Could not save the name: ${err instanceof Error ? err.message : String(err)}`);
2129
2210
  }
2130
- }, [ensureSessions, pushMessage, setTextPrompt]);
2211
+ }, [materializeConversation, pushMessage, setTextPrompt]);
2131
2212
  const submitTextPrompt = useCallback((value) => {
2132
2213
  const prompt = textPromptRef.current;
2133
2214
  if (!prompt)
@@ -2195,8 +2276,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2195
2276
  conversationMutationRef.current = 'fork';
2196
2277
  setConversationMutation('fork');
2197
2278
  try {
2198
- const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2199
- const scope = scopeRef.current;
2279
+ const scope = await materializeConversation();
2280
+ const sessions = sessionsRef.current;
2200
2281
  if (!sessions || !scope) {
2201
2282
  pushMessage('system', 'Conversation history is unavailable in this folder.');
2202
2283
  return;
@@ -2223,9 +2304,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2223
2304
  setConversationMutation(null);
2224
2305
  }
2225
2306
  }, [
2226
- ensureSessions,
2227
2307
  goalActivation,
2228
2308
  hasUnsettledTurn,
2309
+ materializeConversation,
2229
2310
  pushMessage,
2230
2311
  resetSubagentActivity,
2231
2312
  wakeGoalDriver,
@@ -2374,10 +2455,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2374
2455
  switch (event.kind) {
2375
2456
  case 'delta': {
2376
2457
  setState('thinking');
2377
- if (event.messageId && event.runId) {
2458
+ if (event.messageId && event.runId && st.sessionId) {
2378
2459
  lastAssistantMessage.current = {
2379
2460
  runId: event.runId,
2380
2461
  messageId: event.messageId,
2462
+ sessionId: st.sessionId,
2381
2463
  };
2382
2464
  }
2383
2465
  st.text += event.text;
@@ -2441,7 +2523,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2441
2523
  activeToolsRef.current = [...running.slice(0, i), ...running.slice(i + 1)];
2442
2524
  setActiveTools(activeToolsRef.current);
2443
2525
  }
2444
- pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, event.isError ? '✗' : '✓', done?.detail, event.isError ? theme.status.error : theme.status.ok, done ? formatElapsed(Date.now() - done.startedAt) : undefined);
2526
+ pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, event.isError ? '✗' : '✓', done?.detail, event.isError ? theme.status.error : theme.status.ok, event.durationMs !== undefined
2527
+ ? formatElapsed(event.durationMs)
2528
+ : done
2529
+ ? formatElapsed(Date.now() - done.startedAt)
2530
+ : undefined);
2445
2531
  if (event.isError ||
2446
2532
  (!event.hidden && (event.summary.length > 0 || (event.detail?.length ?? 0) > 0))) {
2447
2533
  pushMessage('tool', event.isError ? `failed: ${event.summary}` : event.summary, false, '⎿', event.detail);
@@ -2533,13 +2619,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2533
2619
  // state idle while the admission read was pending let a second submit
2534
2620
  // start beside it and broke the queue's FIFO ownership.
2535
2621
  setState('thinking');
2622
+ let durableScope;
2623
+ try {
2624
+ durableScope = await materializeConversation();
2625
+ }
2626
+ catch (error) {
2627
+ pushMessage('system', `Conversation persistence could not start: ${error instanceof Error ? error.message : String(error)}. This turn will continue in memory and cannot be resumed.`);
2628
+ }
2536
2629
  // A conversation can be closed outside this App after it was opened or
2537
2630
  // resumed. Re-check at the actual turn boundary, before expanding file
2538
2631
  // mentions, recording run evidence, or creating a provider iterator.
2539
2632
  // The persistence helpers repeat the same check at their own boundary;
2540
2633
  // neither read is claimed to serialize a concurrent archive (that needs
2541
2634
  // a store-level lease shared with ProjectManager).
2542
- const destination = scopeRef.current?.sessionId ?? null;
2635
+ const destination = conversationMaterializedRef.current
2636
+ ? (durableScope?.sessionId ?? scopeRef.current?.sessionId ?? null)
2637
+ : null;
2543
2638
  const turnSessions = sessionsRef.current;
2544
2639
  if (turnSessions && destination) {
2545
2640
  try {
@@ -2591,6 +2686,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2591
2686
  pending: '',
2592
2687
  completed: false,
2593
2688
  outcome: null,
2689
+ sessionId: destination,
2594
2690
  notification: null,
2595
2691
  };
2596
2692
  const ac = new AbortController();
@@ -2917,6 +3013,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2917
3013
  finalizeMessage,
2918
3014
  flushStream,
2919
3015
  goalActivation,
3016
+ materializeConversation,
2920
3017
  onPermission,
2921
3018
  pushMessage,
2922
3019
  replaceQueued,
@@ -2941,7 +3038,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2941
3038
  ? 'branched for prompt editing'
2942
3039
  : conversationMutationRef.current === 'archive'
2943
3040
  ? 'archived'
2944
- : 'moved to a fresh conversation';
3041
+ : conversationMutationRef.current === 'materialize'
3042
+ ? 'initialized'
3043
+ : 'moved to a fresh conversation';
2945
3044
  pushMessage('system', `Conversation history is being ${operation}. Wait for it to finish before sending another command or prompt.`);
2946
3045
  return;
2947
3046
  }
@@ -2978,15 +3077,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2978
3077
  });
2979
3078
  return;
2980
3079
  }
2981
- case 'agent-picker': {
3080
+ case 'agent-cockpit': {
2982
3081
  const agents = subagentsRef.current;
2983
3082
  if (agents.length === 0) {
2984
3083
  pushMessage('system', 'No delegated agents are available in this conversation yet. When an Agent tool starts one, /agent opens its live activity and retained transcript.');
2985
3084
  return;
2986
3085
  }
3086
+ const firstPhase = agentPhases(agents)[0];
3087
+ const firstAgent = firstPhase?.agents[0];
3088
+ if (!firstPhase || !firstAgent)
3089
+ return;
2987
3090
  setAgentSurface({
2988
- kind: 'picker',
2989
- selectedId: agents[0]?.viewId ?? '',
3091
+ kind: 'cockpit',
3092
+ selectedPhaseId: firstPhase.id,
3093
+ selectedId: firstAgent.viewId,
3094
+ focus: 'agents',
2990
3095
  });
2991
3096
  return;
2992
3097
  }
@@ -3324,29 +3429,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3324
3429
  // THIS session can answer from. The descriptors used for
3325
3430
  // the merge above carry no store — they are names — so the
3326
3431
  // registry is rebuilt here with the live one.
3327
- const durableSessions = sessionsRef.current;
3328
- const runScope = scopeRef.current;
3329
3432
  const generation = conversationGenRef.current;
3330
3433
  const goalCommand = slash.name === 'goal';
3331
3434
  if (goalCommand)
3332
3435
  goalCommandInFlightRef.current = true;
3333
- const registry = new HostCommandRegistry();
3334
- registry.register(kernelHostCommands({
3335
- allowedAgentIds: session?.agentIds ?? [],
3336
- ...(durableSessions && runScope
3337
- ? {
3338
- goal: {
3339
- store: durableSessions.goals,
3340
- sessionId: runScope.sessionId,
3341
- tenantId: durableSessions.tenantId,
3342
- activation: goalActivation,
3343
- },
3344
- }
3345
- : {}),
3346
- }));
3347
3436
  void (async () => {
3437
+ let goalCommandReleased = false;
3438
+ const releaseGoalCommand = () => {
3439
+ if (!goalCommand || goalCommandReleased)
3440
+ return;
3441
+ goalCommandReleased = true;
3442
+ goalCommandInFlightRef.current = false;
3443
+ wakeGoalDriver();
3444
+ };
3348
3445
  try {
3446
+ if (goalCommand)
3447
+ await materializeConversation();
3448
+ const durableSessions = sessionsRef.current;
3449
+ const runScope = scopeRef.current;
3450
+ const registry = new HostCommandRegistry();
3451
+ registry.register(kernelHostCommands({
3452
+ allowedAgentIds: session?.agentIds ?? [],
3453
+ ...(durableSessions && runScope && conversationMaterializedRef.current
3454
+ ? {
3455
+ goal: {
3456
+ store: durableSessions.goals,
3457
+ sessionId: runScope.sessionId,
3458
+ tenantId: durableSessions.tenantId,
3459
+ activation: goalActivation,
3460
+ },
3461
+ }
3462
+ : {}),
3463
+ }));
3349
3464
  const outcome = await registry.dispatch(`/${slash.name} ${slash.args.join(' ')}`.trim());
3465
+ // The dispatch result is the durable boundary. Release the input
3466
+ // gate before publishing that result: once the operator can see a
3467
+ // completed `/goal` row, the next goal command must be admissible.
3468
+ // Keeping the gate until `finally` left one render-sized window in
3469
+ // which a visible success was followed by "still reaching durable
3470
+ // state" when the operator immediately typed `/goal resume`.
3471
+ releaseGoalCommand();
3350
3472
  // A late readout from the conversation just left must not be
3351
3473
  // painted as state of the one now on screen.
3352
3474
  if (conversationGenRef.current !== generation)
@@ -3361,10 +3483,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3361
3483
  pushMessage('system', `Could not run /${slash.name}: ${error instanceof Error ? error.message : String(error)}`);
3362
3484
  }
3363
3485
  finally {
3364
- if (goalCommand) {
3365
- goalCommandInFlightRef.current = false;
3366
- wakeGoalDriver();
3367
- }
3486
+ releaseGoalCommand();
3368
3487
  }
3369
3488
  })();
3370
3489
  return;
@@ -3382,6 +3501,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3382
3501
  title: 'Rate the latest answer',
3383
3502
  runId: target.runId,
3384
3503
  messageId: target.messageId,
3504
+ sessionId: target.sessionId,
3385
3505
  values,
3386
3506
  options: [
3387
3507
  {
@@ -3399,7 +3519,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3399
3519
  pushMessage('system', 'Nothing to rate yet.');
3400
3520
  return;
3401
3521
  }
3402
- recordFeedback(target.runId, slash.messageId, slash.rating, slash.note);
3522
+ recordFeedback(target.sessionId, target.runId, slash.messageId, slash.rating, slash.note);
3403
3523
  return;
3404
3524
  }
3405
3525
  case 'review': {
@@ -4115,21 +4235,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4115
4235
  if (agentSurfaceCommittedRef.current !== agentView)
4116
4236
  return;
4117
4237
  const agents = subagentsRef.current;
4118
- const index = Math.max(0, agents.findIndex((agent) => agent.viewId === agentView.selectedId));
4119
- const selected = agents[index];
4120
- if (!selected) {
4121
- setAgentSurface(null);
4122
- return;
4123
- }
4124
- if (agentView.kind === 'picker') {
4238
+ if (agentView.kind === 'cockpit') {
4239
+ const phases = agentPhases(agents);
4240
+ const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
4241
+ const phase = phases[phaseIndex];
4242
+ const phaseAgents = phase?.agents ?? [];
4243
+ const agentIndex = Math.max(0, phaseAgents.findIndex((agent) => agent.viewId === agentView.selectedId));
4244
+ const selected = phaseAgents[agentIndex];
4245
+ if (!phase || !selected) {
4246
+ setAgentSurface(null);
4247
+ return;
4248
+ }
4125
4249
  if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
4126
4250
  setAgentSurface(null);
4127
4251
  return;
4128
4252
  }
4253
+ if (key.leftArrow || key.rightArrow || key.tab) {
4254
+ const focus = key.leftArrow
4255
+ ? 'phases'
4256
+ : key.rightArrow
4257
+ ? 'agents'
4258
+ : agentView.focus === 'phases'
4259
+ ? 'agents'
4260
+ : 'phases';
4261
+ setAgentSurface({ ...agentView, focus });
4262
+ return;
4263
+ }
4129
4264
  if (key.return) {
4265
+ if (agentView.focus === 'phases') {
4266
+ setAgentSurface({ ...agentView, focus: 'agents' });
4267
+ return;
4268
+ }
4130
4269
  setAgentSurface({
4131
4270
  kind: 'transcript',
4271
+ selectedPhaseId: phase.id,
4132
4272
  selectedId: selected.viewId,
4273
+ returnFocus: agentView.focus,
4133
4274
  tailOffset: 0,
4134
4275
  });
4135
4276
  return;
@@ -4146,19 +4287,43 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4146
4287
  : key.upArrow
4147
4288
  ? 'previous'
4148
4289
  : 'next';
4149
- const next = moveSelection(index, agents.length, movement, agentPickerPageSize(terminal.rows));
4150
- const target = agents[next];
4151
- if (target)
4152
- setAgentSurface({ kind: 'picker', selectedId: target.viewId });
4290
+ if (agentView.focus === 'phases') {
4291
+ const next = moveSelection(phaseIndex, phases.length, movement, agentPhasePageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
4292
+ const targetPhase = phases[next];
4293
+ const targetAgent = targetPhase?.agents[0];
4294
+ if (targetPhase && targetAgent) {
4295
+ setAgentSurface({
4296
+ ...agentView,
4297
+ selectedPhaseId: targetPhase.id,
4298
+ selectedId: targetAgent.viewId,
4299
+ });
4300
+ }
4301
+ }
4302
+ else {
4303
+ const next = moveSelection(agentIndex, phaseAgents.length, movement, agentPickerPageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
4304
+ const target = phaseAgents[next];
4305
+ if (target)
4306
+ setAgentSurface({ ...agentView, selectedId: target.viewId });
4307
+ }
4153
4308
  }
4154
4309
  return;
4155
4310
  }
4311
+ const selected = agents.find((agent) => agent.viewId === agentView.selectedId);
4312
+ if (!selected) {
4313
+ setAgentSurface(null);
4314
+ return;
4315
+ }
4156
4316
  if (input.toLowerCase() === 'q' || (key.ctrl && input === 'c')) {
4157
4317
  setAgentSurface(null);
4158
4318
  return;
4159
4319
  }
4160
4320
  if (key.escape) {
4161
- setAgentSurface({ kind: 'picker', selectedId: selected.viewId });
4321
+ setAgentSurface({
4322
+ kind: 'cockpit',
4323
+ selectedPhaseId: agentView.selectedPhaseId,
4324
+ selectedId: selected.viewId,
4325
+ focus: agentView.returnFocus,
4326
+ });
4162
4327
  return;
4163
4328
  }
4164
4329
  if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
@@ -4383,8 +4548,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4383
4548
  ? 'compacting conversation — input is paused'
4384
4549
  : permission
4385
4550
  ? hintForPhase(phase, state, session?.hasProvider === true)
4386
- : agentSurface?.kind === 'picker'
4387
- ? 'agent list — enter inspect · esc return'
4551
+ : agentSurface?.kind === 'cockpit'
4552
+ ? agentSurface.focus === 'phases'
4553
+ ? 'agent phases — enter agents · esc return'
4554
+ : 'agents — enter inspect · left phases · esc return'
4388
4555
  : agentSurface?.kind === 'transcript'
4389
4556
  ? 'observing agent — esc agents · q parent'
4390
4557
  : textPrompt
@@ -4396,7 +4563,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4396
4563
  : goalStatus && phase === 'ready' && state === 'idle'
4397
4564
  ? undefined
4398
4565
  : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
4399
- return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: activeTools, working: state === 'thinking' || state === 'tool', agentCount: liveSubagentCount, interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, permission === null && agentSurface?.kind === 'picker' ? (_jsx(AgentPicker, { agents: subagents, selectedId: agentSurface.selectedId, terminalRows: terminal.rows })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
4566
+ return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: activeTools, working: state === 'thinking' || state === 'tool', agentCount: liveSubagentCount, interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
4400
4567
  phase === 'ready' &&
4401
4568
  conversationMutation === null &&
4402
4569
  textPrompt === null &&