@namzu/cli 16.0.0 → 16.1.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 (44) hide show
  1. package/README.md +20 -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/state.d.ts +9 -0
  6. package/dist/commands/state.d.ts.map +1 -0
  7. package/dist/commands/state.js +110 -0
  8. package/dist/commands/state.js.map +1 -0
  9. package/dist/integrations/sessions/store.d.ts.map +1 -1
  10. package/dist/integrations/sessions/store.js +25 -2
  11. package/dist/integrations/sessions/store.js.map +1 -1
  12. package/dist/integrations/state/private-directory.d.ts +15 -0
  13. package/dist/integrations/state/private-directory.d.ts.map +1 -0
  14. package/dist/integrations/state/private-directory.js +41 -0
  15. package/dist/integrations/state/private-directory.js.map +1 -0
  16. package/dist/integrations/state/report.d.ts +125 -0
  17. package/dist/integrations/state/report.d.ts.map +1 -0
  18. package/dist/integrations/state/report.js +784 -0
  19. package/dist/integrations/state/report.js.map +1 -0
  20. package/dist/integrations/subagents/activity.d.ts +18 -0
  21. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  22. package/dist/integrations/subagents/activity.js +64 -7
  23. package/dist/integrations/subagents/activity.js.map +1 -1
  24. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  25. package/dist/integrations/subagents/runtime.js +45 -7
  26. package/dist/integrations/subagents/runtime.js.map +1 -1
  27. package/dist/tui/AgentExplorer.d.ts +26 -5
  28. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  29. package/dist/tui/AgentExplorer.js +125 -14
  30. package/dist/tui/AgentExplorer.js.map +1 -1
  31. package/dist/tui/App.d.ts.map +1 -1
  32. package/dist/tui/App.js +231 -65
  33. package/dist/tui/App.js.map +1 -1
  34. package/dist/tui/agent.d.ts.map +1 -1
  35. package/dist/tui/agent.js +18 -2
  36. package/dist/tui/agent.js.map +1 -1
  37. package/dist/tui/slashCommands.d.ts +1 -1
  38. package/dist/tui/slashCommands.d.ts.map +1 -1
  39. package/dist/tui/slashCommands.js +2 -2
  40. package/dist/tui/slashCommands.js.map +1 -1
  41. package/dist/user-commands/store.d.ts.map +1 -1
  42. package/dist/user-commands/store.js +26 -5
  43. package/dist/user-commands/store.js.map +1 -1
  44. 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') {
@@ -1323,9 +1358,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1323
1358
  ? { text: persistedOutput, provenance: 'persisted' }
1324
1359
  : null;
1325
1360
  initialConversationIdRef.current = undefined;
1361
+ conversationMaterializedRef.current = true;
1326
1362
  }
1327
1363
  else {
1328
- sessionId = await startConversation(sessions);
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();
1329
1367
  }
1330
1368
  sessionsRef.current = sessions;
1331
1369
  scopeRef.current = {
@@ -1342,6 +1380,45 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1342
1380
  return undefined;
1343
1381
  }
1344
1382
  }, [nextId]);
1383
+ /** Publish the provisional conversation exactly once, at first durable use. */
1384
+ const materializeConversation = useCallback(async () => {
1385
+ const existing = await ensureSessions();
1386
+ if (!existing)
1387
+ return undefined;
1388
+ if (conversationMaterializedRef.current)
1389
+ return existing;
1390
+ if (materializationRef.current)
1391
+ return materializationRef.current;
1392
+ const pending = (async () => {
1393
+ const sessions = sessionsRef.current;
1394
+ const scope = scopeRef.current;
1395
+ if (!sessions || !scope)
1396
+ return undefined;
1397
+ conversationMutationRef.current = 'materialize';
1398
+ try {
1399
+ const sessionId = await startConversation(sessions);
1400
+ if (scopeRef.current !== scope || conversationMaterializedRef.current) {
1401
+ throw new Error('the active conversation changed while its first session was published');
1402
+ }
1403
+ scope.sessionId = sessionId;
1404
+ conversationMaterializedRef.current = true;
1405
+ return scope;
1406
+ }
1407
+ finally {
1408
+ if (conversationMutationRef.current === 'materialize') {
1409
+ conversationMutationRef.current = null;
1410
+ }
1411
+ }
1412
+ })();
1413
+ materializationRef.current = pending;
1414
+ try {
1415
+ return await pending;
1416
+ }
1417
+ finally {
1418
+ if (materializationRef.current === pending)
1419
+ materializationRef.current = null;
1420
+ }
1421
+ }, [ensureSessions]);
1345
1422
  const hydrateSession = useCallback(async (prefs, detectedNow, signal) => {
1346
1423
  if (signal?.aborted)
1347
1424
  return;
@@ -1985,11 +2062,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1985
2062
  setMessages(restored);
1986
2063
  modelHistoryRef.current = msgs;
1987
2064
  setHistory((previous) => [...previous, ...promptHistoryFromConversation(msgs)]);
2065
+ // Persisted conversation rows do not carry the live run/message identity
2066
+ // required by feedback validation. Do not leave the answer from the
2067
+ // conversation we just exited as a hidden `/feedback` target.
2068
+ lastAssistantMessage.current = null;
1988
2069
  const persistedOutput = latestAssistantOutput(msgs);
1989
2070
  lastCompletedOutputRef.current = persistedOutput
1990
2071
  ? { text: persistedOutput, provenance: 'persisted' }
1991
2072
  : null;
1992
2073
  scope.sessionId = conv.id; // new turns now attribute to the resumed session
2074
+ conversationMaterializedRef.current = true;
1993
2075
  pushMessage('system', `Resumed: ${conv.title}`);
1994
2076
  if (discardedQueued > 0) {
1995
2077
  pushMessage('system', `Discarded ${discardedQueued} queued prompt${discardedQueued === 1 ? '' : 's'} from the conversation you left.`);
@@ -2053,8 +2135,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2053
2135
  modelHistoryRef.current = [];
2054
2136
  lastAssistantMessage.current = null;
2055
2137
  lastCompletedOutputRef.current = null;
2056
- if (sourceScope && targetSessionId)
2138
+ if (sourceScope && targetSessionId) {
2057
2139
  sourceScope.sessionId = targetSessionId;
2140
+ conversationMaterializedRef.current = true;
2141
+ }
2058
2142
  if (clearScreen) {
2059
2143
  setMessages([]);
2060
2144
  resetTranscript();
@@ -2098,8 +2182,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2098
2182
  * history; it is a session-store operation with the session id captured here.
2099
2183
  */
2100
2184
  const doTitle = useCallback(async (title, clear) => {
2101
- const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2102
- const scope = scopeRef.current;
2185
+ const scope = await materializeConversation();
2186
+ const sessions = sessionsRef.current;
2103
2187
  if (!sessions || !scope) {
2104
2188
  pushMessage('system', 'Conversation history is unavailable in this folder.');
2105
2189
  return;
@@ -2127,7 +2211,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2127
2211
  catch (err) {
2128
2212
  pushMessage('system', `Could not save the name: ${err instanceof Error ? err.message : String(err)}`);
2129
2213
  }
2130
- }, [ensureSessions, pushMessage, setTextPrompt]);
2214
+ }, [materializeConversation, pushMessage, setTextPrompt]);
2131
2215
  const submitTextPrompt = useCallback((value) => {
2132
2216
  const prompt = textPromptRef.current;
2133
2217
  if (!prompt)
@@ -2195,8 +2279,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2195
2279
  conversationMutationRef.current = 'fork';
2196
2280
  setConversationMutation('fork');
2197
2281
  try {
2198
- const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2199
- const scope = scopeRef.current;
2282
+ const scope = await materializeConversation();
2283
+ const sessions = sessionsRef.current;
2200
2284
  if (!sessions || !scope) {
2201
2285
  pushMessage('system', 'Conversation history is unavailable in this folder.');
2202
2286
  return;
@@ -2223,9 +2307,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2223
2307
  setConversationMutation(null);
2224
2308
  }
2225
2309
  }, [
2226
- ensureSessions,
2227
2310
  goalActivation,
2228
2311
  hasUnsettledTurn,
2312
+ materializeConversation,
2229
2313
  pushMessage,
2230
2314
  resetSubagentActivity,
2231
2315
  wakeGoalDriver,
@@ -2374,10 +2458,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2374
2458
  switch (event.kind) {
2375
2459
  case 'delta': {
2376
2460
  setState('thinking');
2377
- if (event.messageId && event.runId) {
2461
+ if (event.messageId && event.runId && st.sessionId) {
2378
2462
  lastAssistantMessage.current = {
2379
2463
  runId: event.runId,
2380
2464
  messageId: event.messageId,
2465
+ sessionId: st.sessionId,
2381
2466
  };
2382
2467
  }
2383
2468
  st.text += event.text;
@@ -2533,13 +2618,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2533
2618
  // state idle while the admission read was pending let a second submit
2534
2619
  // start beside it and broke the queue's FIFO ownership.
2535
2620
  setState('thinking');
2621
+ let durableScope;
2622
+ try {
2623
+ durableScope = await materializeConversation();
2624
+ }
2625
+ catch (error) {
2626
+ pushMessage('system', `Conversation persistence could not start: ${error instanceof Error ? error.message : String(error)}. This turn will continue in memory and cannot be resumed.`);
2627
+ }
2536
2628
  // A conversation can be closed outside this App after it was opened or
2537
2629
  // resumed. Re-check at the actual turn boundary, before expanding file
2538
2630
  // mentions, recording run evidence, or creating a provider iterator.
2539
2631
  // The persistence helpers repeat the same check at their own boundary;
2540
2632
  // neither read is claimed to serialize a concurrent archive (that needs
2541
2633
  // a store-level lease shared with ProjectManager).
2542
- const destination = scopeRef.current?.sessionId ?? null;
2634
+ const destination = conversationMaterializedRef.current
2635
+ ? (durableScope?.sessionId ?? scopeRef.current?.sessionId ?? null)
2636
+ : null;
2543
2637
  const turnSessions = sessionsRef.current;
2544
2638
  if (turnSessions && destination) {
2545
2639
  try {
@@ -2591,6 +2685,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2591
2685
  pending: '',
2592
2686
  completed: false,
2593
2687
  outcome: null,
2688
+ sessionId: destination,
2594
2689
  notification: null,
2595
2690
  };
2596
2691
  const ac = new AbortController();
@@ -2917,6 +3012,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2917
3012
  finalizeMessage,
2918
3013
  flushStream,
2919
3014
  goalActivation,
3015
+ materializeConversation,
2920
3016
  onPermission,
2921
3017
  pushMessage,
2922
3018
  replaceQueued,
@@ -2941,7 +3037,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2941
3037
  ? 'branched for prompt editing'
2942
3038
  : conversationMutationRef.current === 'archive'
2943
3039
  ? 'archived'
2944
- : 'moved to a fresh conversation';
3040
+ : conversationMutationRef.current === 'materialize'
3041
+ ? 'initialized'
3042
+ : 'moved to a fresh conversation';
2945
3043
  pushMessage('system', `Conversation history is being ${operation}. Wait for it to finish before sending another command or prompt.`);
2946
3044
  return;
2947
3045
  }
@@ -2978,15 +3076,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2978
3076
  });
2979
3077
  return;
2980
3078
  }
2981
- case 'agent-picker': {
3079
+ case 'agent-cockpit': {
2982
3080
  const agents = subagentsRef.current;
2983
3081
  if (agents.length === 0) {
2984
3082
  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
3083
  return;
2986
3084
  }
3085
+ const firstPhase = agentPhases(agents)[0];
3086
+ const firstAgent = firstPhase?.agents[0];
3087
+ if (!firstPhase || !firstAgent)
3088
+ return;
2987
3089
  setAgentSurface({
2988
- kind: 'picker',
2989
- selectedId: agents[0]?.viewId ?? '',
3090
+ kind: 'cockpit',
3091
+ selectedPhaseId: firstPhase.id,
3092
+ selectedId: firstAgent.viewId,
3093
+ focus: 'agents',
2990
3094
  });
2991
3095
  return;
2992
3096
  }
@@ -3324,29 +3428,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3324
3428
  // THIS session can answer from. The descriptors used for
3325
3429
  // the merge above carry no store — they are names — so the
3326
3430
  // registry is rebuilt here with the live one.
3327
- const durableSessions = sessionsRef.current;
3328
- const runScope = scopeRef.current;
3329
3431
  const generation = conversationGenRef.current;
3330
3432
  const goalCommand = slash.name === 'goal';
3331
3433
  if (goalCommand)
3332
3434
  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
3435
  void (async () => {
3436
+ let goalCommandReleased = false;
3437
+ const releaseGoalCommand = () => {
3438
+ if (!goalCommand || goalCommandReleased)
3439
+ return;
3440
+ goalCommandReleased = true;
3441
+ goalCommandInFlightRef.current = false;
3442
+ wakeGoalDriver();
3443
+ };
3348
3444
  try {
3445
+ if (goalCommand)
3446
+ await materializeConversation();
3447
+ const durableSessions = sessionsRef.current;
3448
+ const runScope = scopeRef.current;
3449
+ const registry = new HostCommandRegistry();
3450
+ registry.register(kernelHostCommands({
3451
+ allowedAgentIds: session?.agentIds ?? [],
3452
+ ...(durableSessions && runScope && conversationMaterializedRef.current
3453
+ ? {
3454
+ goal: {
3455
+ store: durableSessions.goals,
3456
+ sessionId: runScope.sessionId,
3457
+ tenantId: durableSessions.tenantId,
3458
+ activation: goalActivation,
3459
+ },
3460
+ }
3461
+ : {}),
3462
+ }));
3349
3463
  const outcome = await registry.dispatch(`/${slash.name} ${slash.args.join(' ')}`.trim());
3464
+ // The dispatch result is the durable boundary. Release the input
3465
+ // gate before publishing that result: once the operator can see a
3466
+ // completed `/goal` row, the next goal command must be admissible.
3467
+ // Keeping the gate until `finally` left one render-sized window in
3468
+ // which a visible success was followed by "still reaching durable
3469
+ // state" when the operator immediately typed `/goal resume`.
3470
+ releaseGoalCommand();
3350
3471
  // A late readout from the conversation just left must not be
3351
3472
  // painted as state of the one now on screen.
3352
3473
  if (conversationGenRef.current !== generation)
@@ -3361,10 +3482,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3361
3482
  pushMessage('system', `Could not run /${slash.name}: ${error instanceof Error ? error.message : String(error)}`);
3362
3483
  }
3363
3484
  finally {
3364
- if (goalCommand) {
3365
- goalCommandInFlightRef.current = false;
3366
- wakeGoalDriver();
3367
- }
3485
+ releaseGoalCommand();
3368
3486
  }
3369
3487
  })();
3370
3488
  return;
@@ -3382,6 +3500,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3382
3500
  title: 'Rate the latest answer',
3383
3501
  runId: target.runId,
3384
3502
  messageId: target.messageId,
3503
+ sessionId: target.sessionId,
3385
3504
  values,
3386
3505
  options: [
3387
3506
  {
@@ -3399,7 +3518,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3399
3518
  pushMessage('system', 'Nothing to rate yet.');
3400
3519
  return;
3401
3520
  }
3402
- recordFeedback(target.runId, slash.messageId, slash.rating, slash.note);
3521
+ recordFeedback(target.sessionId, target.runId, slash.messageId, slash.rating, slash.note);
3403
3522
  return;
3404
3523
  }
3405
3524
  case 'review': {
@@ -4115,21 +4234,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4115
4234
  if (agentSurfaceCommittedRef.current !== agentView)
4116
4235
  return;
4117
4236
  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') {
4237
+ if (agentView.kind === 'cockpit') {
4238
+ const phases = agentPhases(agents);
4239
+ const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
4240
+ const phase = phases[phaseIndex];
4241
+ const phaseAgents = phase?.agents ?? [];
4242
+ const agentIndex = Math.max(0, phaseAgents.findIndex((agent) => agent.viewId === agentView.selectedId));
4243
+ const selected = phaseAgents[agentIndex];
4244
+ if (!phase || !selected) {
4245
+ setAgentSurface(null);
4246
+ return;
4247
+ }
4125
4248
  if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
4126
4249
  setAgentSurface(null);
4127
4250
  return;
4128
4251
  }
4252
+ if (key.leftArrow || key.rightArrow || key.tab) {
4253
+ const focus = key.leftArrow
4254
+ ? 'phases'
4255
+ : key.rightArrow
4256
+ ? 'agents'
4257
+ : agentView.focus === 'phases'
4258
+ ? 'agents'
4259
+ : 'phases';
4260
+ setAgentSurface({ ...agentView, focus });
4261
+ return;
4262
+ }
4129
4263
  if (key.return) {
4264
+ if (agentView.focus === 'phases') {
4265
+ setAgentSurface({ ...agentView, focus: 'agents' });
4266
+ return;
4267
+ }
4130
4268
  setAgentSurface({
4131
4269
  kind: 'transcript',
4270
+ selectedPhaseId: phase.id,
4132
4271
  selectedId: selected.viewId,
4272
+ returnFocus: agentView.focus,
4133
4273
  tailOffset: 0,
4134
4274
  });
4135
4275
  return;
@@ -4146,19 +4286,43 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4146
4286
  : key.upArrow
4147
4287
  ? 'previous'
4148
4288
  : '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 });
4289
+ if (agentView.focus === 'phases') {
4290
+ const next = moveSelection(phaseIndex, phases.length, movement, agentPhasePageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
4291
+ const targetPhase = phases[next];
4292
+ const targetAgent = targetPhase?.agents[0];
4293
+ if (targetPhase && targetAgent) {
4294
+ setAgentSurface({
4295
+ ...agentView,
4296
+ selectedPhaseId: targetPhase.id,
4297
+ selectedId: targetAgent.viewId,
4298
+ });
4299
+ }
4300
+ }
4301
+ else {
4302
+ const next = moveSelection(agentIndex, phaseAgents.length, movement, agentPickerPageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
4303
+ const target = phaseAgents[next];
4304
+ if (target)
4305
+ setAgentSurface({ ...agentView, selectedId: target.viewId });
4306
+ }
4153
4307
  }
4154
4308
  return;
4155
4309
  }
4310
+ const selected = agents.find((agent) => agent.viewId === agentView.selectedId);
4311
+ if (!selected) {
4312
+ setAgentSurface(null);
4313
+ return;
4314
+ }
4156
4315
  if (input.toLowerCase() === 'q' || (key.ctrl && input === 'c')) {
4157
4316
  setAgentSurface(null);
4158
4317
  return;
4159
4318
  }
4160
4319
  if (key.escape) {
4161
- setAgentSurface({ kind: 'picker', selectedId: selected.viewId });
4320
+ setAgentSurface({
4321
+ kind: 'cockpit',
4322
+ selectedPhaseId: agentView.selectedPhaseId,
4323
+ selectedId: selected.viewId,
4324
+ focus: agentView.returnFocus,
4325
+ });
4162
4326
  return;
4163
4327
  }
4164
4328
  if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
@@ -4383,8 +4547,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4383
4547
  ? 'compacting conversation — input is paused'
4384
4548
  : permission
4385
4549
  ? hintForPhase(phase, state, session?.hasProvider === true)
4386
- : agentSurface?.kind === 'picker'
4387
- ? 'agent list — enter inspect · esc return'
4550
+ : agentSurface?.kind === 'cockpit'
4551
+ ? agentSurface.focus === 'phases'
4552
+ ? 'agent phases — enter agents · esc return'
4553
+ : 'agents — enter inspect · left phases · esc return'
4388
4554
  : agentSurface?.kind === 'transcript'
4389
4555
  ? 'observing agent — esc agents · q parent'
4390
4556
  : textPrompt
@@ -4396,7 +4562,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4396
4562
  : goalStatus && phase === 'ready' && state === 'idle'
4397
4563
  ? undefined
4398
4564
  : 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' &&
4565
+ 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
4566
  phase === 'ready' &&
4401
4567
  conversationMutation === null &&
4402
4568
  textPrompt === null &&