@namzu/cli 25.0.1 → 26.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.
- package/CHANGELOG.md +265 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +5 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +5 -0
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +5 -0
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +5 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +47 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +24 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/tool-result-screens.d.ts +103 -0
- package/dist/config/tool-result-screens.d.ts.map +1 -0
- package/dist/config/tool-result-screens.js +104 -0
- package/dist/config/tool-result-screens.js.map +1 -0
- package/dist/context/doctrine.d.ts +4 -2
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +4 -2
- package/dist/context/doctrine.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts +30 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +32 -1
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/resident/session-step.d.ts.map +1 -1
- package/dist/integrations/resident/session-step.js +5 -0
- package/dist/integrations/resident/session-step.js.map +1 -1
- package/dist/integrations/subagents/activity.d.ts +285 -11
- package/dist/integrations/subagents/activity.d.ts.map +1 -1
- package/dist/integrations/subagents/activity.js +334 -31
- package/dist/integrations/subagents/activity.js.map +1 -1
- package/dist/integrations/subagents/replay.d.ts +46 -0
- package/dist/integrations/subagents/replay.d.ts.map +1 -0
- package/dist/integrations/subagents/replay.js +193 -0
- package/dist/integrations/subagents/replay.js.map +1 -0
- package/dist/integrations/subagents/runs.d.ts +72 -0
- package/dist/integrations/subagents/runs.d.ts.map +1 -0
- package/dist/integrations/subagents/runs.js +155 -0
- package/dist/integrations/subagents/runs.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +21 -0
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +134 -2
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/tui/AgentExplorer.d.ts +46 -2
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +261 -22
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +456 -28
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/ChoicePicker.d.ts +2 -0
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +3 -1
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +10 -7
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +2 -7
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/StatusBar.d.ts +56 -10
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +106 -35
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/agent.d.ts +46 -1
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +55 -5
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +3 -1
- package/dist/tui/choice-selection.d.ts.map +1 -1
- package/dist/tui/choice-selection.js +2 -2
- package/dist/tui/choice-selection.js.map +1 -1
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +14 -2
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +40 -0
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +62 -3
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/types.d.ts +12 -0
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/tui/App.js
CHANGED
|
@@ -29,6 +29,8 @@ import { terminalNotificationEnabled, writeTerminalNotification, } from '../inte
|
|
|
29
29
|
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
30
30
|
import { appendMessages, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
31
31
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
32
|
+
import { liveOrchestrationRuns } from '../integrations/subagents/runs.js';
|
|
33
|
+
import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
|
|
32
34
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
33
35
|
import { checkUpdates } from '../integrations/updates.js';
|
|
34
36
|
import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
|
|
@@ -36,7 +38,7 @@ import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
|
|
|
36
38
|
import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
|
|
37
39
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
38
40
|
import { discoverUserCommands } from '../user-commands/store.js';
|
|
39
|
-
import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
41
|
+
import { AgentCockpit, AgentNarrationBand, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
40
42
|
import { BrandHeader } from './BrandHeader.js';
|
|
41
43
|
import { ChoicePicker } from './ChoicePicker.js';
|
|
42
44
|
import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
|
|
@@ -76,7 +78,7 @@ import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows
|
|
|
76
78
|
import { describeRunInterruption, describeRunStop } from './run-interruption.js';
|
|
77
79
|
import { moveSelection } from './selection-window.js';
|
|
78
80
|
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
79
|
-
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
81
|
+
import { baseBranchReviewPrompt, combineOrchestrationRuns, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgentRuns, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
80
82
|
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
81
83
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
82
84
|
import { theme } from './theme.js';
|
|
@@ -387,6 +389,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
387
389
|
reasoningEffortRef.current = next;
|
|
388
390
|
setReasoningEffortState(next);
|
|
389
391
|
}, []);
|
|
392
|
+
// A session setting layered above effort, not a level a provider publishes
|
|
393
|
+
// — see docs/cli/slash-commands.md's /effort section. In-memory and
|
|
394
|
+
// per-session like reasoningEffort above; no preferences file involved.
|
|
395
|
+
const [orchestrateMode, setOrchestrateModeState] = useState(false);
|
|
396
|
+
const orchestrateModeRef = useRef(false);
|
|
397
|
+
const setOrchestrateMode = useCallback((next) => {
|
|
398
|
+
orchestrateModeRef.current = next;
|
|
399
|
+
setOrchestrateModeState(next);
|
|
400
|
+
}, []);
|
|
390
401
|
const [activeSkills, setActiveSkills] = useState([]);
|
|
391
402
|
// Read when the session comes up rather than on every keystroke: the
|
|
392
403
|
// autocomplete dropdown consults this on each character, and a `readdirSync`
|
|
@@ -586,6 +597,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
586
597
|
/** Bounded child-run projection published by the current AgentSession. */
|
|
587
598
|
const [subagents, setSubagentsState] = useState([]);
|
|
588
599
|
const subagentsRef = useRef([]);
|
|
600
|
+
/**
|
|
601
|
+
* The parent's own commentary on the work it is coordinating, from the same
|
|
602
|
+
* monitor and the same subscription as the rows below it. Read through the
|
|
603
|
+
* optional reader, so a source that publishes no narration is simply a
|
|
604
|
+
* session with none.
|
|
605
|
+
*/
|
|
606
|
+
const [narration, setNarration] = useState([]);
|
|
589
607
|
const [agentSurface, setAgentSurfaceState] = useState(null);
|
|
590
608
|
const agentSurfaceRef = useRef(null);
|
|
591
609
|
const agentSurfaceCommittedRef = useRef(null);
|
|
@@ -604,7 +622,30 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
604
622
|
useEffect(() => {
|
|
605
623
|
agentSurfaceCommittedRef.current = agentSurface;
|
|
606
624
|
}, [agentSurface]);
|
|
607
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Children of this conversation's earlier runs, rebuilt from saved
|
|
627
|
+
* evidence, kept apart from the live monitor's own snapshot.
|
|
628
|
+
*
|
|
629
|
+
* A ref and not state because it is an input to `replaceSubagents`, which
|
|
630
|
+
* runs from the monitor's subscription: holding it as state would make
|
|
631
|
+
* every saved row arrive one render after the live rows it belongs beside.
|
|
632
|
+
*/
|
|
633
|
+
const savedSubagentsRef = useRef([]);
|
|
634
|
+
/** Revokes an in-flight evidence read whose session or conversation is gone. */
|
|
635
|
+
const savedSubagentsTokenRef = useRef(0);
|
|
636
|
+
const withSavedSubagents = useCallback((live) => {
|
|
637
|
+
const saved = savedSubagentsRef.current;
|
|
638
|
+
if (saved.length === 0)
|
|
639
|
+
return live;
|
|
640
|
+
// A child the live monitor still holds wins over its own saved copy:
|
|
641
|
+
// both describe one run, and only one of the two is attached to
|
|
642
|
+
// anything. Saved rows land after the live ones, so a cohort that is
|
|
643
|
+
// still running keeps the top of the list.
|
|
644
|
+
const liveRunIds = new Set(live.map((agent) => agent.runId).filter((id) => id !== undefined));
|
|
645
|
+
return [...live, ...saved.filter((agent) => !agent.runId || !liveRunIds.has(agent.runId))];
|
|
646
|
+
}, []);
|
|
647
|
+
const replaceSubagents = useCallback((incoming) => {
|
|
648
|
+
const next = withSavedSubagents(incoming);
|
|
608
649
|
const previous = subagentsRef.current;
|
|
609
650
|
subagentsRef.current = next;
|
|
610
651
|
setSubagentsState(next);
|
|
@@ -632,8 +673,40 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
632
673
|
selectedId: fallbackAgent.viewId,
|
|
633
674
|
focus: surface.kind === 'cockpit' ? surface.focus : surface.returnFocus,
|
|
634
675
|
});
|
|
635
|
-
}, [setAgentSurface]);
|
|
636
|
-
|
|
676
|
+
}, [setAgentSurface, withSavedSubagents]);
|
|
677
|
+
/**
|
|
678
|
+
* Re-reads what this conversation's finished children left on disk.
|
|
679
|
+
*
|
|
680
|
+
* Called when a session is installed, whenever the conversation the scope
|
|
681
|
+
* names changes — a resume is the whole point of this: the children worth
|
|
682
|
+
* opening belong to the runs of the conversation just switched TO — and
|
|
683
|
+
* whenever the cockpit is opened, which is what recovers a child the live
|
|
684
|
+
* monitor's eighty-agent bound has since evicted. All three are reads of
|
|
685
|
+
* directories that are already there; nothing is created, and a failure
|
|
686
|
+
* leaves the live rows untouched.
|
|
687
|
+
*/
|
|
688
|
+
const hydrateSavedChildren = useCallback(async () => {
|
|
689
|
+
const read = session?.savedChildren;
|
|
690
|
+
if (!read)
|
|
691
|
+
return;
|
|
692
|
+
const token = ++savedSubagentsTokenRef.current;
|
|
693
|
+
let saved;
|
|
694
|
+
try {
|
|
695
|
+
saved = await read();
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// Saved evidence is an extra view over a directory that may not exist
|
|
699
|
+
// yet, or may not be readable. Not finding it is not a reason to
|
|
700
|
+
// disturb a session whose live children are fine.
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (token !== savedSubagentsTokenRef.current)
|
|
704
|
+
return;
|
|
705
|
+
savedSubagentsRef.current = saved;
|
|
706
|
+
replaceSubagents(session?.subagents?.getSnapshot() ?? []);
|
|
707
|
+
}, [replaceSubagents, session]);
|
|
708
|
+
/** Opens the cockpit on the rows already on screen; false when there are none. */
|
|
709
|
+
const openCockpitFromCurrentRows = useCallback(() => {
|
|
637
710
|
const agents = subagentsRef.current;
|
|
638
711
|
const workflows = agentWorkflows(agents);
|
|
639
712
|
const currentWorkflow = [...workflows].reverse().find((workflow) => workflow.agents.some((agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working')) ?? workflows.at(-1);
|
|
@@ -649,6 +722,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
649
722
|
});
|
|
650
723
|
return true;
|
|
651
724
|
}, [setAgentSurface]);
|
|
725
|
+
/** The most recent cockpit request, so an older one cannot report on its behalf. */
|
|
726
|
+
const cockpitOpenTokenRef = useRef(0);
|
|
727
|
+
/**
|
|
728
|
+
* Ctrl+T and `/agents`: open the cockpit, and when nothing is on screen
|
|
729
|
+
* yet, WAIT for the saved evidence before answering.
|
|
730
|
+
*
|
|
731
|
+
* The waiting is the point. A conversation's children may exist only on
|
|
732
|
+
* disk — evicted by the eighty-agent bound, or written by a process that
|
|
733
|
+
* has since exited — and that read is asynchronous. Answering from the
|
|
734
|
+
* rows already up would make the first press after a resume report an
|
|
735
|
+
* absence that the read, still in flight, was about to disprove. Rows that
|
|
736
|
+
* ARE up open immediately and refresh the saved ones behind the screen, so
|
|
737
|
+
* the common case pays nothing for this.
|
|
738
|
+
*/
|
|
739
|
+
const openAgentCockpit = useCallback(async () => {
|
|
740
|
+
if (openCockpitFromCurrentRows()) {
|
|
741
|
+
void hydrateSavedChildren();
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
const token = ++cockpitOpenTokenRef.current;
|
|
745
|
+
await hydrateSavedChildren();
|
|
746
|
+
// A later request owns the screen now. Reporting `false` here would put
|
|
747
|
+
// an empty-state line under a cockpit that request is about to open.
|
|
748
|
+
if (token !== cockpitOpenTokenRef.current)
|
|
749
|
+
return true;
|
|
750
|
+
return openCockpitFromCurrentRows();
|
|
751
|
+
}, [hydrateSavedChildren, openCockpitFromCurrentRows]);
|
|
652
752
|
/** One git-backed review choice may be resolving while its visible picker remains authoritative. */
|
|
653
753
|
const reviewChoiceInFlightRef = useRef(null);
|
|
654
754
|
const exitArmedRef = useRef(false);
|
|
@@ -703,17 +803,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
703
803
|
useEffect(() => {
|
|
704
804
|
const source = session?.subagents;
|
|
705
805
|
setAgentSurface(null);
|
|
806
|
+
savedSubagentsRef.current = [];
|
|
807
|
+
void hydrateSavedChildren();
|
|
706
808
|
if (!source) {
|
|
707
809
|
replaceSubagents([]);
|
|
810
|
+
setNarration([]);
|
|
708
811
|
return;
|
|
709
812
|
}
|
|
710
|
-
const refresh = () =>
|
|
813
|
+
const refresh = () => {
|
|
814
|
+
replaceSubagents(source.getSnapshot());
|
|
815
|
+
setNarration(source.getNarration?.() ?? []);
|
|
816
|
+
};
|
|
711
817
|
refresh();
|
|
712
818
|
return source.subscribe(refresh);
|
|
713
|
-
}, [replaceSubagents, session, setAgentSurface]);
|
|
819
|
+
}, [hydrateSavedChildren, replaceSubagents, session, setAgentSurface]);
|
|
714
820
|
const resetSubagentActivity = useCallback(() => {
|
|
715
821
|
session?.subagents?.reset();
|
|
716
822
|
setAgentSurface(null);
|
|
823
|
+
// The monitor clears its own commentary on reset; this drops the copy
|
|
824
|
+
// already on screen in the same pass as the rows, so the band cannot
|
|
825
|
+
// outlive the conversation it was written in.
|
|
826
|
+
setNarration([]);
|
|
827
|
+
// A new conversation starts with no children on screen, saved ones
|
|
828
|
+
// included: the evidence stays on disk, but it belongs to the runs of
|
|
829
|
+
// the conversation that was just left behind.
|
|
830
|
+
savedSubagentsTokenRef.current += 1;
|
|
831
|
+
savedSubagentsRef.current = [];
|
|
717
832
|
replaceSubagents([]);
|
|
718
833
|
}, [replaceSubagents, session, setAgentSurface]);
|
|
719
834
|
// Source of truth for in-flight tools (the event loop runs across renders, so
|
|
@@ -1002,6 +1117,92 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1002
1117
|
]);
|
|
1003
1118
|
return id;
|
|
1004
1119
|
}, [nextId]);
|
|
1120
|
+
/**
|
|
1121
|
+
* `/agents runs`: opens a picker immediately with a disabled loading row,
|
|
1122
|
+
* then replaces it once the live snapshot and the disk walk both resolve.
|
|
1123
|
+
*
|
|
1124
|
+
* The composer stays reachable the whole time — nothing here awaits before
|
|
1125
|
+
* the first `setChoicePicker`. A stale read is dropped by comparing
|
|
1126
|
+
* `choicePickerRef.current` against the exact loading-picker object this
|
|
1127
|
+
* call put there (the idiom the `/review` pickers already use below),
|
|
1128
|
+
* rather than a counter: Escape and every other picker-replacing path
|
|
1129
|
+
* already route through `setChoicePicker`, so they retire this object as a
|
|
1130
|
+
* side effect and need no separate token bump of their own. That covers
|
|
1131
|
+
* both "the operator left this picker" (any other object is current now,
|
|
1132
|
+
* `null` included) and "opened it again" (a second call's own object is
|
|
1133
|
+
* current instead).
|
|
1134
|
+
*/
|
|
1135
|
+
const openAgentRunsPicker = useCallback(() => {
|
|
1136
|
+
setSelectedChoice(0);
|
|
1137
|
+
const pending = {
|
|
1138
|
+
kind: 'agent-runs',
|
|
1139
|
+
title: 'Orchestration runs',
|
|
1140
|
+
values: ['loading'],
|
|
1141
|
+
options: [
|
|
1142
|
+
{ label: 'Loading…', description: 'Reading saved evidence…', disabledReason: 'Loading…' },
|
|
1143
|
+
],
|
|
1144
|
+
};
|
|
1145
|
+
setChoicePicker(pending);
|
|
1146
|
+
void (async () => {
|
|
1147
|
+
const live = liveOrchestrationRuns(subagentsRef.current, Date.now());
|
|
1148
|
+
let finished = [];
|
|
1149
|
+
try {
|
|
1150
|
+
finished = (await session?.listOrchestrationRuns?.()) ?? [];
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
// Disk evidence is optional here: a live-only listing is still an
|
|
1154
|
+
// honest answer, and the empty-history message below still applies
|
|
1155
|
+
// if that live half turns out empty too.
|
|
1156
|
+
}
|
|
1157
|
+
if (appLifetime.signal.aborted || choicePickerRef.current !== pending)
|
|
1158
|
+
return;
|
|
1159
|
+
const listing = combineOrchestrationRuns(live, finished);
|
|
1160
|
+
const empty = renderAgentRuns(listing);
|
|
1161
|
+
if (empty) {
|
|
1162
|
+
setChoicePicker(null);
|
|
1163
|
+
pushMessage('system', empty);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
setChoicePicker({
|
|
1167
|
+
kind: 'agent-runs',
|
|
1168
|
+
title: 'Orchestration runs',
|
|
1169
|
+
notice: listing.omitted > 0
|
|
1170
|
+
? `${listing.omitted} older run${listing.omitted === 1 ? '' : 's'} not shown.`
|
|
1171
|
+
: undefined,
|
|
1172
|
+
values: listing.runs,
|
|
1173
|
+
options: listing.runs.map(agentRunOption),
|
|
1174
|
+
});
|
|
1175
|
+
})();
|
|
1176
|
+
}, [appLifetime, pushMessage, session, setChoicePicker, setSelectedChoice]);
|
|
1177
|
+
/**
|
|
1178
|
+
* Opens an `/agents runs` selection straight into its first agent's
|
|
1179
|
+
* transcript — where the replayed banner actually lives, for a finished
|
|
1180
|
+
* run — rather than the phase/agent list Ctrl+T lands on.
|
|
1181
|
+
*
|
|
1182
|
+
* A finished run's evidence is hydrated first, since a run this operator
|
|
1183
|
+
* has not opened the cockpit for yet may not be in `subagents` at all.
|
|
1184
|
+
* A live run skips that: it is already there, and re-reading disk for a
|
|
1185
|
+
* run this process is still writing would risk the very half-written
|
|
1186
|
+
* evidence `listSavedOrchestrationRuns` exists to stay off of.
|
|
1187
|
+
*/
|
|
1188
|
+
const openOrchestrationRun = useCallback(async (run) => {
|
|
1189
|
+
if (!run.live)
|
|
1190
|
+
await hydrateSavedChildren();
|
|
1191
|
+
const workflow = agentWorkflows(subagentsRef.current).find((candidate) => candidate.agents.some((agent) => agent.workflowId === run.id));
|
|
1192
|
+
const firstPhase = workflow?.phases[0];
|
|
1193
|
+
const firstAgent = firstPhase?.agents[0];
|
|
1194
|
+
if (!firstPhase || !firstAgent) {
|
|
1195
|
+
pushMessage('system', `Could not open "${run.name}" — its saved evidence is no longer available.`);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
setAgentSurface({
|
|
1199
|
+
kind: 'transcript',
|
|
1200
|
+
selectedPhaseId: firstPhase.id,
|
|
1201
|
+
selectedId: firstAgent.viewId,
|
|
1202
|
+
returnFocus: 'agents',
|
|
1203
|
+
tailOffset: 0,
|
|
1204
|
+
});
|
|
1205
|
+
}, [hydrateSavedChildren, pushMessage, setAgentSurface]);
|
|
1005
1206
|
const reportedAgentsRef = useRef(new Set());
|
|
1006
1207
|
useEffect(() => {
|
|
1007
1208
|
reportedAgentsRef.current.clear();
|
|
@@ -1025,6 +1226,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1025
1226
|
pushMessage('tool', `${agent.description} · ${status}`, false, completed ? '✓' : '✗', undefined, completed ? theme.status.ok : theme.status.error, 'ctrl+t · agent details');
|
|
1026
1227
|
}
|
|
1027
1228
|
}, [subagents, pushMessage, session]);
|
|
1229
|
+
/**
|
|
1230
|
+
* A delivered `send_message` correction already lands its own row inside
|
|
1231
|
+
* the child's transcript (`SubagentActivityMonitor.recordMessage`); this
|
|
1232
|
+
* mirrors it into the main conversation so the operator sees what was
|
|
1233
|
+
* sent without drilling in. Deliberately a separate ref from
|
|
1234
|
+
* `reportedAgentsRef` above: that gate is exactly-once per terminal
|
|
1235
|
+
* agent, this one is exactly-once per delivered message, and folding the
|
|
1236
|
+
* two together would let one gate's reset silently double-report the
|
|
1237
|
+
* other's rows.
|
|
1238
|
+
*/
|
|
1239
|
+
const reportedCorrectionsRef = useRef(new Set());
|
|
1240
|
+
useEffect(() => {
|
|
1241
|
+
reportedCorrectionsRef.current.clear();
|
|
1242
|
+
}, [session]);
|
|
1243
|
+
useEffect(() => {
|
|
1244
|
+
const current = session?.subagents?.getSnapshot() ?? [];
|
|
1245
|
+
for (const agent of subagents) {
|
|
1246
|
+
const live = current.find((item) => item.viewId === agent.viewId && item.workflowId === agent.workflowId);
|
|
1247
|
+
if (!live)
|
|
1248
|
+
continue;
|
|
1249
|
+
for (const row of agent.transcript) {
|
|
1250
|
+
if (row.kind !== 'system' || row.direction !== 'to-child')
|
|
1251
|
+
continue;
|
|
1252
|
+
if (!live.transcript.some((item) => item.id === row.id))
|
|
1253
|
+
continue;
|
|
1254
|
+
if (reportedCorrectionsRef.current.has(row.id))
|
|
1255
|
+
continue;
|
|
1256
|
+
reportedCorrectionsRef.current.add(row.id);
|
|
1257
|
+
pushMessage('tool', `${agent.description} · correction sent`, false, '→', [row.text]);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}, [subagents, pushMessage, session]);
|
|
1028
1261
|
const applyPermissionMode = useCallback((mode) => {
|
|
1029
1262
|
if (!session?.hasProvider) {
|
|
1030
1263
|
pushMessage('system', 'Choose a model before changing permissions.');
|
|
@@ -1079,6 +1312,47 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1079
1312
|
setReasoningEffort(effort);
|
|
1080
1313
|
pushMessage('system', `Reasoning: ${effort} (this session).`);
|
|
1081
1314
|
}, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
|
|
1315
|
+
/**
|
|
1316
|
+
* Turn the session's orchestrate mode on or off.
|
|
1317
|
+
*
|
|
1318
|
+
* On: pins effort to the model's highest published level (last entry of
|
|
1319
|
+
* `reasoningEffortLevels`, which every provider publishes low-to-high) and
|
|
1320
|
+
* strengthens delegation guidance for future turns via `SendOptions.orchestrate`
|
|
1321
|
+
* — see `applyReasoningEffort` above for the mirrored guard shape. When the
|
|
1322
|
+
* model publishes no exact menu, effort is left alone and the notice says
|
|
1323
|
+
* so explicitly rather than silently doing nothing.
|
|
1324
|
+
*/
|
|
1325
|
+
const applyOrchestrateMode = useCallback((enabled, selectedSession) => {
|
|
1326
|
+
if (!session?.hasProvider) {
|
|
1327
|
+
pushMessage('system', 'No active session — pick a provider before turning orchestrate mode on.');
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (selectedSession !== undefined && selectedSession !== session) {
|
|
1331
|
+
pushMessage('system', 'Orchestrate mode was not changed: the selected model is no longer active.');
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (state !== 'idle' ||
|
|
1335
|
+
abortRef.current !== null ||
|
|
1336
|
+
hasUnsettledTurn() ||
|
|
1337
|
+
(queuedRef.current.length > 0 && selectedSession !== session) ||
|
|
1338
|
+
permissionResolveRef.current !== null ||
|
|
1339
|
+
compactingRef.current) {
|
|
1340
|
+
pushMessage('system', 'Orchestrate mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
setOrchestrateMode(enabled);
|
|
1344
|
+
if (!enabled) {
|
|
1345
|
+
pushMessage('system', 'Orchestrate mode is off.');
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const highest = highestReasoningEffort(session.reasoningEffortLevels);
|
|
1349
|
+
if (highest !== undefined) {
|
|
1350
|
+
setReasoningEffort(highest);
|
|
1351
|
+
pushMessage('system', `Orchestrate mode is on — effort pinned to ${highest} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
pushMessage('system', `Orchestrate mode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
|
|
1355
|
+
}, [hasUnsettledTurn, pushMessage, session, setOrchestrateMode, setReasoningEffort, state]);
|
|
1082
1356
|
const stepReasoningEffort = useCallback((direction) => {
|
|
1083
1357
|
const levels = session?.reasoningEffortLevels;
|
|
1084
1358
|
if (!session?.hasProvider || levels === undefined || levels.length === 0) {
|
|
@@ -1583,6 +1857,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1583
1857
|
setChoicePicker({ ...child, back: picker });
|
|
1584
1858
|
return;
|
|
1585
1859
|
}
|
|
1860
|
+
if (picker.kind === 'agent-runs') {
|
|
1861
|
+
if (value === 'loading')
|
|
1862
|
+
return;
|
|
1863
|
+
void openOrchestrationRun(value);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1586
1866
|
if (picker.kind === 'archive-conversation') {
|
|
1587
1867
|
if (value === 'archive')
|
|
1588
1868
|
archiveCurrentConversation();
|
|
@@ -1636,16 +1916,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1636
1916
|
removeStoredCredential(value);
|
|
1637
1917
|
return;
|
|
1638
1918
|
}
|
|
1919
|
+
if (picker.kind === 'reasoning-effort' && value === ORCHESTRATE_MODE_VALUE) {
|
|
1920
|
+
applyOrchestrateMode(!orchestrateModeRef.current, picker.selectedSession);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1639
1923
|
applyReasoningEffort(value, picker.selectedSession);
|
|
1640
1924
|
}, [
|
|
1641
1925
|
activateSkill,
|
|
1642
1926
|
advanceQueueContinuation,
|
|
1643
1927
|
appLifetime.signal,
|
|
1928
|
+
applyOrchestrateMode,
|
|
1644
1929
|
applyPermissionMode,
|
|
1645
1930
|
applyReasoningEffort,
|
|
1646
1931
|
archiveCurrentConversation,
|
|
1647
1932
|
ctx.cwd,
|
|
1648
1933
|
enqueueQueued,
|
|
1934
|
+
openOrchestrationRun,
|
|
1649
1935
|
pushMessage,
|
|
1650
1936
|
recordFeedback,
|
|
1651
1937
|
removeStoredCredential,
|
|
@@ -1863,6 +2149,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1863
2149
|
...(activeCtx.memory ? { memory: activeCtx.memory } : {}),
|
|
1864
2150
|
...(activeCtx.limits ? { limits: activeCtx.limits } : {}),
|
|
1865
2151
|
...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
|
|
2152
|
+
// The operator's screens, on the same terms as every other
|
|
2153
|
+
// config field the App forwards. `!== undefined` rather than a
|
|
2154
|
+
// length test: `[]` is the off switch, and dropping it here
|
|
2155
|
+
// would leave the kernel's default in force in the interactive
|
|
2156
|
+
// session — always on, for the one operator who said otherwise.
|
|
2157
|
+
...(activeCtx.toolResultScreens !== undefined
|
|
2158
|
+
? { toolResultScreens: activeCtx.toolResultScreens }
|
|
2159
|
+
: {}),
|
|
1866
2160
|
// Somebody is at this terminal, so the model may ask them one
|
|
1867
2161
|
// question when a decision is genuinely theirs.
|
|
1868
2162
|
askUser: true,
|
|
@@ -1905,9 +2199,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1905
2199
|
// A picker-owned provider/model change is one state transition. Clear the
|
|
1906
2200
|
// old model's effort selection before publishing the replacement session
|
|
1907
2201
|
// or releasing any paused queue. Failed and superseded candidates returned
|
|
1908
|
-
// above, so they leave the current session selection untouched.
|
|
1909
|
-
|
|
1910
|
-
|
|
2202
|
+
// above, so they leave the current session selection untouched. Orchestrate
|
|
2203
|
+
// mode survives the switch: re-pin to the new model's highest published
|
|
2204
|
+
// level instead of clearing, exactly as it pinned when first turned on.
|
|
2205
|
+
if (signal !== undefined) {
|
|
2206
|
+
setReasoningEffort(orchestrateModeRef.current ? highestReasoningEffort(s.reasoningEffortLevels) : undefined);
|
|
2207
|
+
}
|
|
1911
2208
|
// Re-hydration (a provider switch via /model) builds a second session;
|
|
1912
2209
|
// without this the first one's tool-server child processes stay alive
|
|
1913
2210
|
// for the rest of the TUI's life.
|
|
@@ -1915,7 +2212,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1915
2212
|
void previousSessionRef.current?.close();
|
|
1916
2213
|
previousSessionRef.current = s;
|
|
1917
2214
|
setSession(s);
|
|
1918
|
-
|
|
2215
|
+
// Orchestrate mode already re-pinned effort above; reopening this picker
|
|
2216
|
+
// would ask the operator to redo a choice the mode just made for them.
|
|
2217
|
+
if (!orchestrateModeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
|
|
1919
2218
|
// Own input before publishing ready or releasing a paused queue. The
|
|
1920
2219
|
// menu closes only after choosing an effort or keeping the new default.
|
|
1921
2220
|
setSelectedChoice(0);
|
|
@@ -2603,6 +2902,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2603
2902
|
: null;
|
|
2604
2903
|
pendingGoalResumeRef.current = conv.id;
|
|
2605
2904
|
scope.sessionId = conv.id; // new turns now attribute to the resumed session
|
|
2905
|
+
// The scope now names the resumed conversation, so its finished
|
|
2906
|
+
// children are readable. Without this the first Ctrl+T after a resume
|
|
2907
|
+
// would have nothing to find.
|
|
2908
|
+
void hydrateSavedChildren();
|
|
2606
2909
|
conversationMaterializedRef.current = true;
|
|
2607
2910
|
pushMessage('system', `Resumed: ${conv.title}`);
|
|
2608
2911
|
if (discardedQueued > 0) {
|
|
@@ -2623,6 +2926,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2623
2926
|
nextId,
|
|
2624
2927
|
pushMessage,
|
|
2625
2928
|
cancelPendingModelSwitch,
|
|
2929
|
+
hydrateSavedChildren,
|
|
2626
2930
|
resetSubagentActivity,
|
|
2627
2931
|
session,
|
|
2628
2932
|
resetTranscript,
|
|
@@ -2674,6 +2978,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2674
2978
|
if (sourceScope && targetSessionId) {
|
|
2675
2979
|
sourceScope.sessionId = targetSessionId;
|
|
2676
2980
|
conversationMaterializedRef.current = true;
|
|
2981
|
+
// A fresh conversation owns no runs yet, so this normally reads
|
|
2982
|
+
// nothing. It runs anyway because the rule is "the scope changed,
|
|
2983
|
+
// re-read", and an exception here is how the resume path lost
|
|
2984
|
+
// its own re-read.
|
|
2985
|
+
void hydrateSavedChildren();
|
|
2677
2986
|
}
|
|
2678
2987
|
if (clearScreen) {
|
|
2679
2988
|
setMessages([]);
|
|
@@ -2706,6 +3015,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2706
3015
|
interruptTurn,
|
|
2707
3016
|
pushMessage,
|
|
2708
3017
|
cancelPendingModelSwitch,
|
|
3018
|
+
hydrateSavedChildren,
|
|
2709
3019
|
resetSubagentActivity,
|
|
2710
3020
|
session,
|
|
2711
3021
|
resetTranscript,
|
|
@@ -2858,6 +3168,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2858
3168
|
// is reloaded or reset. Only where the NEXT turn is written changes.
|
|
2859
3169
|
scope.sessionId = forked.id;
|
|
2860
3170
|
resetSubagentActivity();
|
|
3171
|
+
void hydrateSavedChildren();
|
|
2861
3172
|
session?.resetTaskStore?.();
|
|
2862
3173
|
goalActivation.clear();
|
|
2863
3174
|
wakeGoalDriver();
|
|
@@ -2875,6 +3186,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2875
3186
|
hasUnsettledTurn,
|
|
2876
3187
|
materializeConversation,
|
|
2877
3188
|
pushMessage,
|
|
3189
|
+
hydrateSavedChildren,
|
|
2878
3190
|
resetSubagentActivity,
|
|
2879
3191
|
session,
|
|
2880
3192
|
wakeGoalDriver,
|
|
@@ -2968,6 +3280,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2968
3280
|
goalActivation.clear();
|
|
2969
3281
|
wakeGoalDriver();
|
|
2970
3282
|
scope.sessionId = forked.id;
|
|
3283
|
+
void hydrateSavedChildren();
|
|
2971
3284
|
modelHistoryRef.current = forked.messages;
|
|
2972
3285
|
lastAssistantMessage.current = null;
|
|
2973
3286
|
const persistedOutput = latestAssistantOutput(forked.messages);
|
|
@@ -3005,6 +3318,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3005
3318
|
nextId,
|
|
3006
3319
|
pushMessage,
|
|
3007
3320
|
cancelPendingModelSwitch,
|
|
3321
|
+
hydrateSavedChildren,
|
|
3008
3322
|
resetSubagentActivity,
|
|
3009
3323
|
session,
|
|
3010
3324
|
resetTranscript,
|
|
@@ -3229,6 +3543,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3229
3543
|
event.toolName.toLowerCase() === 'agent' &&
|
|
3230
3544
|
subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
|
|
3231
3545
|
(event.runId === undefined || agent.workflowId === event.runId));
|
|
3546
|
+
// The band above the rail already carries a narrated line, in the
|
|
3547
|
+
// parent's own words, from the moment it is written. Printing the
|
|
3548
|
+
// call that wrote it would show the same sentence twice, the
|
|
3549
|
+
// second time as protocol — the same duplicate-protocol case as
|
|
3550
|
+
// the represented Agent call just below, and refused the same
|
|
3551
|
+
// way. A REFUSED line keeps its row: nothing reached the screen,
|
|
3552
|
+
// and that row is the only thing that says so.
|
|
3553
|
+
if (!event.isError && event.toolName === NARRATION_TOOL_NAME) {
|
|
3554
|
+
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
3555
|
+
break;
|
|
3556
|
+
}
|
|
3232
3557
|
if (representedSuccessfulAgent) {
|
|
3233
3558
|
// The automatic panel already owned this successful child from
|
|
3234
3559
|
// start through settlement. Re-emitting its internal Agent call as
|
|
@@ -3684,6 +4009,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3684
4009
|
activeTurnInboxRef.current = inbox;
|
|
3685
4010
|
const turnPermissionMode = permissionModeRef.current;
|
|
3686
4011
|
const turnReasoningEffort = reasoningEffortRef.current;
|
|
4012
|
+
const turnOrchestrateMode = orchestrateModeRef.current;
|
|
3687
4013
|
const turnRunLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
|
|
3688
4014
|
// Always carry the guarded callback. `auto` and `strict` decide before
|
|
3689
4015
|
// calling it in makeResumeHandler; retaining it is what lets a session
|
|
@@ -3744,6 +4070,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3744
4070
|
permissionMode: turnPermissionMode,
|
|
3745
4071
|
limits: turnRunLimits,
|
|
3746
4072
|
...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
|
|
4073
|
+
...(turnOrchestrateMode ? { orchestrate: true } : {}),
|
|
3747
4074
|
...(goalRound ? { goalRound } : {}),
|
|
3748
4075
|
// The mode above decides whether this callback is consulted.
|
|
3749
4076
|
onPermission: askPermission,
|
|
@@ -4224,9 +4551,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4224
4551
|
return;
|
|
4225
4552
|
}
|
|
4226
4553
|
case 'agent-cockpit': {
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4554
|
+
// Reported after the read, not before it: the answer to "are
|
|
4555
|
+
// there any" may be on disk, and a conversation whose evidence
|
|
4556
|
+
// is sitting in its own directory must never be told it
|
|
4557
|
+
// delegated nothing.
|
|
4558
|
+
const agentIds = session?.agentIds ?? [];
|
|
4559
|
+
void openAgentCockpit().then((opened) => {
|
|
4560
|
+
if (opened)
|
|
4561
|
+
return;
|
|
4562
|
+
pushMessage('system', `No delegated agents in this conversation.${agentIds.length ? `\nAvailable: ${agentIds.join(', ')}` : '\nNo agents are configured.'}`);
|
|
4563
|
+
});
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4566
|
+
case 'agent-runs': {
|
|
4567
|
+
openAgentRunsPicker();
|
|
4230
4568
|
return;
|
|
4231
4569
|
}
|
|
4232
4570
|
case 'provider-setup':
|
|
@@ -4563,7 +4901,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4563
4901
|
const values = [undefined, ...levels];
|
|
4564
4902
|
const current = reasoningEffortRef.current;
|
|
4565
4903
|
setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
|
|
4566
|
-
setChoicePicker(reasoningEffortPicker(session, current) ?? null);
|
|
4904
|
+
setChoicePicker(reasoningEffortPicker(session, current, false, orchestrateModeRef.current) ?? null);
|
|
4567
4905
|
return;
|
|
4568
4906
|
}
|
|
4569
4907
|
case 'login':
|
|
@@ -4981,6 +5319,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4981
5319
|
: 'Raw output mode off — rich transcript rendering restored.');
|
|
4982
5320
|
return;
|
|
4983
5321
|
}
|
|
5322
|
+
case 'orchestrate-mode': {
|
|
5323
|
+
const enabled = slash.enabled === 'toggle' ? !orchestrateModeRef.current : slash.enabled;
|
|
5324
|
+
applyOrchestrateMode(enabled);
|
|
5325
|
+
return;
|
|
5326
|
+
}
|
|
4984
5327
|
case 'export-picker': {
|
|
4985
5328
|
if (!stableExportSource())
|
|
4986
5329
|
return;
|
|
@@ -5063,6 +5406,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5063
5406
|
hostCommands,
|
|
5064
5407
|
nextId,
|
|
5065
5408
|
openAgentCockpit,
|
|
5409
|
+
openAgentRunsPicker,
|
|
5066
5410
|
pushMessage,
|
|
5067
5411
|
rawOutput,
|
|
5068
5412
|
removeStoredCredential,
|
|
@@ -5563,8 +5907,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5563
5907
|
setAgentSurface(null);
|
|
5564
5908
|
return;
|
|
5565
5909
|
}
|
|
5566
|
-
if (
|
|
5910
|
+
if (openCockpitFromCurrentRows()) {
|
|
5911
|
+
void hydrateSavedChildren();
|
|
5567
5912
|
return;
|
|
5913
|
+
}
|
|
5914
|
+
// Nothing on screen yet. The conversation's children may exist only
|
|
5915
|
+
// as saved evidence, so the request outlives that read rather than
|
|
5916
|
+
// being answered by an empty snapshot.
|
|
5917
|
+
void openAgentCockpit();
|
|
5568
5918
|
}
|
|
5569
5919
|
// Child observation owns navigation, but never outranks consent: the
|
|
5570
5920
|
// permission branch above preempts it if a child or parent call asks.
|
|
@@ -5702,7 +6052,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5702
6052
|
return;
|
|
5703
6053
|
}
|
|
5704
6054
|
if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
|
|
5705
|
-
|
|
6055
|
+
// Reserving the replayed banner's row here too keeps one PgDn a
|
|
6056
|
+
// screenful of what is actually visible.
|
|
6057
|
+
const page = agentTranscriptPageSize(terminal.rows, selected.replayed ? 1 : 0);
|
|
5706
6058
|
const max = maxAgentTranscriptTailOffset(selected, terminal.rows, terminal.columns);
|
|
5707
6059
|
const offset = Math.min(agentView.tailOffset, max);
|
|
5708
6060
|
const next = key.home
|
|
@@ -5976,6 +6328,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5976
6328
|
? permissionOwner ? `Agent: ${permissionOwner.description}` : `Run: ${permission.runId}`
|
|
5977
6329
|
: undefined;
|
|
5978
6330
|
const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
|
|
6331
|
+
// True exactly when the phase ternary below renders its final branch — the
|
|
6332
|
+
// normal composer view, as opposed to provider setup or a lifecycle
|
|
6333
|
+
// picker/prompt. The agent rail is a feature of that view: computing this
|
|
6334
|
+
// once here lets its render site sit after the footer, below, while still
|
|
6335
|
+
// never appearing during trust/resume/edit/picker/setup phases.
|
|
6336
|
+
const showComposerSurface = !providerSetup &&
|
|
6337
|
+
phase !== 'trust' &&
|
|
6338
|
+
phase !== 'unhealthy' &&
|
|
6339
|
+
phase !== 'resume' &&
|
|
6340
|
+
phase !== 'edit' &&
|
|
6341
|
+
phase !== 'picker';
|
|
5979
6342
|
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, permissionMode: displayedPermissionMode })) : undefined }) }), providerSetup ? (_jsx(ProviderSetup, { cwd: ctx.cwd, onClose: () => setProviderSetup(false), onConnect: () => {
|
|
5980
6343
|
setProviderSetup(false);
|
|
5981
6344
|
setPickerInitialView('providers');
|
|
@@ -6009,7 +6372,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6009
6372
|
? queuePause.outcome === 'paused'
|
|
6010
6373
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
6011
6374
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
6012
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels,
|
|
6375
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
6013
6376
|
phase !== 'ready' ||
|
|
6014
6377
|
state === 'awaiting-permission' ||
|
|
6015
6378
|
compacting ||
|
|
@@ -6026,13 +6389,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6026
6389
|
(state === 'thinking' || state === 'tool' || pendingModelSwitchRef.current !== null), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
|
|
6027
6390
|
...command,
|
|
6028
6391
|
description: command.unavailable?.(slashCtx) ?? command.description,
|
|
6029
|
-
})), mentionCandidates: mentionCandidates, history: history })] }),
|
|
6392
|
+
})), mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, orchestrate: orchestrateMode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
|
|
6393
|
+
permission === null &&
|
|
6394
|
+
agentSurface === null &&
|
|
6395
|
+
outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
|
|
6396
|
+
permission === null &&
|
|
6397
|
+
agentSurface === null &&
|
|
6398
|
+
outputViewer === null &&
|
|
6399
|
+
liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface && permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface &&
|
|
6400
|
+
permission === null &&
|
|
6401
|
+
agentSurface?.kind === 'transcript' &&
|
|
6402
|
+
selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
|
|
6030
6403
|
if (outputIndex > 0)
|
|
6031
6404
|
setOutputViewer(outputBlocks[outputIndex - 1]);
|
|
6032
6405
|
}, onNext: () => {
|
|
6033
6406
|
if (outputIndex >= 0 && outputIndex + 1 < outputBlocks.length)
|
|
6034
6407
|
setOutputViewer(outputBlocks[outputIndex + 1]);
|
|
6035
|
-
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null
|
|
6408
|
+
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null] }) }));
|
|
6036
6409
|
}
|
|
6037
6410
|
/** One line for a background job that ended, for the transcript and the next turn. */
|
|
6038
6411
|
function describeJobExit(job) {
|
|
@@ -6109,6 +6482,33 @@ function goalStatusLabel(goal, armed) {
|
|
|
6109
6482
|
function labelOfOption(question, optionId) {
|
|
6110
6483
|
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
6111
6484
|
}
|
|
6485
|
+
/** `/agents runs`' one-line-per-run text, mirroring `/jobs`' own column shape. */
|
|
6486
|
+
function agentRunOption(run) {
|
|
6487
|
+
const agents = `${run.agentsDone}/${run.agentsTotal} agent${run.agentsTotal === 1 ? '' : 's'}`;
|
|
6488
|
+
const tokens = `${run.tokensTotal.toLocaleString('en-US')} tokens`;
|
|
6489
|
+
const columns = [
|
|
6490
|
+
relativeRunStart(run.startedAt),
|
|
6491
|
+
run.phases.join(', '),
|
|
6492
|
+
agents,
|
|
6493
|
+
tokens,
|
|
6494
|
+
formatElapsed(run.elapsedMs),
|
|
6495
|
+
];
|
|
6496
|
+
if (run.live)
|
|
6497
|
+
columns.push('live');
|
|
6498
|
+
return { label: run.name, description: columns.join(' · ') };
|
|
6499
|
+
}
|
|
6500
|
+
/** `just now` / `12m ago` / `3h ago` / `2d ago` — the same buckets a conversation's own resume picker shows. */
|
|
6501
|
+
function relativeRunStart(startedAt) {
|
|
6502
|
+
const minutes = Math.round((Date.now() - startedAt) / 60_000);
|
|
6503
|
+
if (minutes < 1)
|
|
6504
|
+
return 'just now';
|
|
6505
|
+
if (minutes < 60)
|
|
6506
|
+
return `${minutes}m ago`;
|
|
6507
|
+
const hours = Math.round(minutes / 60);
|
|
6508
|
+
if (hours < 24)
|
|
6509
|
+
return `${hours}h ago`;
|
|
6510
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
6511
|
+
}
|
|
6112
6512
|
function choicePickerSearchable(picker) {
|
|
6113
6513
|
return ['command', 'skill', 'plugin', 'review-branch', 'review-commit'].includes(picker.kind);
|
|
6114
6514
|
}
|
|
@@ -6190,11 +6590,27 @@ function permissionPicker(mode, approvedAll, more = false) {
|
|
|
6190
6590
|
}),
|
|
6191
6591
|
};
|
|
6192
6592
|
}
|
|
6193
|
-
|
|
6593
|
+
/**
|
|
6594
|
+
* The picker's sentinel for the orchestrate-mode row, below the rule.
|
|
6595
|
+
*
|
|
6596
|
+
* A plain string outside the `ReasoningEffort` union by construction — never
|
|
6597
|
+
* added to any model's published menu — so it can share the same picker
|
|
6598
|
+
* without ever being mistaken for a level `/effort <token>` could select.
|
|
6599
|
+
*/
|
|
6600
|
+
const ORCHESTRATE_MODE_VALUE = 'orchestrate';
|
|
6601
|
+
/** Last entry of a low-to-high menu every provider publishes in that order; `undefined` when none is known. */
|
|
6602
|
+
function highestReasoningEffort(levels) {
|
|
6603
|
+
return levels && levels.length > 0 ? levels[levels.length - 1] : undefined;
|
|
6604
|
+
}
|
|
6605
|
+
function reasoningEffortPicker(session, current, afterModelSelection = false, orchestrateOn = false) {
|
|
6194
6606
|
const levels = session.reasoningEffortLevels;
|
|
6195
6607
|
if (!session.hasProvider || levels === undefined)
|
|
6196
6608
|
return undefined;
|
|
6197
|
-
const
|
|
6609
|
+
const effortValues = [undefined, ...levels];
|
|
6610
|
+
const values = [
|
|
6611
|
+
...effortValues,
|
|
6612
|
+
ORCHESTRATE_MODE_VALUE,
|
|
6613
|
+
];
|
|
6198
6614
|
return {
|
|
6199
6615
|
kind: 'reasoning-effort',
|
|
6200
6616
|
title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
|
|
@@ -6205,12 +6621,24 @@ function reasoningEffortPicker(session, current, afterModelSelection = false) {
|
|
|
6205
6621
|
}
|
|
6206
6622
|
: {}),
|
|
6207
6623
|
values,
|
|
6208
|
-
options:
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6624
|
+
options: [
|
|
6625
|
+
...effortValues.map((effort) => ({
|
|
6626
|
+
label: effort ?? 'default',
|
|
6627
|
+
description: reasoningEffortDescription(effort),
|
|
6628
|
+
current: effort === current,
|
|
6629
|
+
default: effort === undefined,
|
|
6630
|
+
})),
|
|
6631
|
+
{
|
|
6632
|
+
// Below a rule, visually apart from the levels above: a session
|
|
6633
|
+
// setting, not a sixth level the provider published.
|
|
6634
|
+
label: 'orchestrate',
|
|
6635
|
+
description: orchestrateOn
|
|
6636
|
+
? 'On · highest level and delegate by default'
|
|
6637
|
+
: 'Off · highest level and delegate by default',
|
|
6638
|
+
current: orchestrateOn,
|
|
6639
|
+
ruleBefore: true,
|
|
6640
|
+
},
|
|
6641
|
+
],
|
|
6214
6642
|
};
|
|
6215
6643
|
}
|
|
6216
6644
|
function reasoningEffortDescription(effort) {
|