@namzu/cli 25.0.0 → 25.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.
- package/CHANGELOG.md +282 -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/context/runtime-message.d.ts.map +1 -1
- package/dist/context/runtime-message.js +4 -0
- package/dist/context/runtime-message.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/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 +121 -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 +458 -32
- 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 +38 -1
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +78 -10
- 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/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`
|
|
@@ -405,10 +416,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
405
416
|
summaries: 0,
|
|
406
417
|
reclaimedTokens: 0,
|
|
407
418
|
});
|
|
408
|
-
// Jobs that ended while no turn was running.
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
419
|
+
// Jobs that ended while no turn was running.
|
|
420
|
+
//
|
|
421
|
+
// Only those: an exit that lands mid-turn belongs to the kernel, which
|
|
422
|
+
// rides it out on the next tool result or, for a job the model awaited,
|
|
423
|
+
// delivers it as the message that releases its own suspend. Queueing one
|
|
424
|
+
// here as well would announce the same exit twice, so the `abortRef`
|
|
425
|
+
// check below is the boundary between the two owners rather than an
|
|
426
|
+
// optimisation. Between turns the kernel is not listening, so the exit is
|
|
427
|
+
// held here and handed to the next send as part of the system text; the
|
|
428
|
+
// transcript row is written at once.
|
|
412
429
|
const idleJobNoticesRef = useRef([]);
|
|
413
430
|
const drainIdleJobNotices = () => {
|
|
414
431
|
if (idleJobNoticesRef.current.length === 0)
|
|
@@ -580,6 +597,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
580
597
|
/** Bounded child-run projection published by the current AgentSession. */
|
|
581
598
|
const [subagents, setSubagentsState] = useState([]);
|
|
582
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([]);
|
|
583
607
|
const [agentSurface, setAgentSurfaceState] = useState(null);
|
|
584
608
|
const agentSurfaceRef = useRef(null);
|
|
585
609
|
const agentSurfaceCommittedRef = useRef(null);
|
|
@@ -598,7 +622,30 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
598
622
|
useEffect(() => {
|
|
599
623
|
agentSurfaceCommittedRef.current = agentSurface;
|
|
600
624
|
}, [agentSurface]);
|
|
601
|
-
|
|
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);
|
|
602
649
|
const previous = subagentsRef.current;
|
|
603
650
|
subagentsRef.current = next;
|
|
604
651
|
setSubagentsState(next);
|
|
@@ -626,8 +673,40 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
626
673
|
selectedId: fallbackAgent.viewId,
|
|
627
674
|
focus: surface.kind === 'cockpit' ? surface.focus : surface.returnFocus,
|
|
628
675
|
});
|
|
629
|
-
}, [setAgentSurface]);
|
|
630
|
-
|
|
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(() => {
|
|
631
710
|
const agents = subagentsRef.current;
|
|
632
711
|
const workflows = agentWorkflows(agents);
|
|
633
712
|
const currentWorkflow = [...workflows].reverse().find((workflow) => workflow.agents.some((agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working')) ?? workflows.at(-1);
|
|
@@ -643,6 +722,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
643
722
|
});
|
|
644
723
|
return true;
|
|
645
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]);
|
|
646
752
|
/** One git-backed review choice may be resolving while its visible picker remains authoritative. */
|
|
647
753
|
const reviewChoiceInFlightRef = useRef(null);
|
|
648
754
|
const exitArmedRef = useRef(false);
|
|
@@ -697,17 +803,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
697
803
|
useEffect(() => {
|
|
698
804
|
const source = session?.subagents;
|
|
699
805
|
setAgentSurface(null);
|
|
806
|
+
savedSubagentsRef.current = [];
|
|
807
|
+
void hydrateSavedChildren();
|
|
700
808
|
if (!source) {
|
|
701
809
|
replaceSubagents([]);
|
|
810
|
+
setNarration([]);
|
|
702
811
|
return;
|
|
703
812
|
}
|
|
704
|
-
const refresh = () =>
|
|
813
|
+
const refresh = () => {
|
|
814
|
+
replaceSubagents(source.getSnapshot());
|
|
815
|
+
setNarration(source.getNarration?.() ?? []);
|
|
816
|
+
};
|
|
705
817
|
refresh();
|
|
706
818
|
return source.subscribe(refresh);
|
|
707
|
-
}, [replaceSubagents, session, setAgentSurface]);
|
|
819
|
+
}, [hydrateSavedChildren, replaceSubagents, session, setAgentSurface]);
|
|
708
820
|
const resetSubagentActivity = useCallback(() => {
|
|
709
821
|
session?.subagents?.reset();
|
|
710
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 = [];
|
|
711
832
|
replaceSubagents([]);
|
|
712
833
|
}, [replaceSubagents, session, setAgentSurface]);
|
|
713
834
|
// Source of truth for in-flight tools (the event loop runs across renders, so
|
|
@@ -996,6 +1117,92 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
996
1117
|
]);
|
|
997
1118
|
return id;
|
|
998
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]);
|
|
999
1206
|
const reportedAgentsRef = useRef(new Set());
|
|
1000
1207
|
useEffect(() => {
|
|
1001
1208
|
reportedAgentsRef.current.clear();
|
|
@@ -1019,6 +1226,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1019
1226
|
pushMessage('tool', `${agent.description} · ${status}`, false, completed ? '✓' : '✗', undefined, completed ? theme.status.ok : theme.status.error, 'ctrl+t · agent details');
|
|
1020
1227
|
}
|
|
1021
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]);
|
|
1022
1261
|
const applyPermissionMode = useCallback((mode) => {
|
|
1023
1262
|
if (!session?.hasProvider) {
|
|
1024
1263
|
pushMessage('system', 'Choose a model before changing permissions.');
|
|
@@ -1073,6 +1312,47 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1073
1312
|
setReasoningEffort(effort);
|
|
1074
1313
|
pushMessage('system', `Reasoning: ${effort} (this session).`);
|
|
1075
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]);
|
|
1076
1356
|
const stepReasoningEffort = useCallback((direction) => {
|
|
1077
1357
|
const levels = session?.reasoningEffortLevels;
|
|
1078
1358
|
if (!session?.hasProvider || levels === undefined || levels.length === 0) {
|
|
@@ -1577,6 +1857,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1577
1857
|
setChoicePicker({ ...child, back: picker });
|
|
1578
1858
|
return;
|
|
1579
1859
|
}
|
|
1860
|
+
if (picker.kind === 'agent-runs') {
|
|
1861
|
+
if (value === 'loading')
|
|
1862
|
+
return;
|
|
1863
|
+
void openOrchestrationRun(value);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1580
1866
|
if (picker.kind === 'archive-conversation') {
|
|
1581
1867
|
if (value === 'archive')
|
|
1582
1868
|
archiveCurrentConversation();
|
|
@@ -1630,16 +1916,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1630
1916
|
removeStoredCredential(value);
|
|
1631
1917
|
return;
|
|
1632
1918
|
}
|
|
1919
|
+
if (picker.kind === 'reasoning-effort' && value === ORCHESTRATE_MODE_VALUE) {
|
|
1920
|
+
applyOrchestrateMode(!orchestrateModeRef.current, picker.selectedSession);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1633
1923
|
applyReasoningEffort(value, picker.selectedSession);
|
|
1634
1924
|
}, [
|
|
1635
1925
|
activateSkill,
|
|
1636
1926
|
advanceQueueContinuation,
|
|
1637
1927
|
appLifetime.signal,
|
|
1928
|
+
applyOrchestrateMode,
|
|
1638
1929
|
applyPermissionMode,
|
|
1639
1930
|
applyReasoningEffort,
|
|
1640
1931
|
archiveCurrentConversation,
|
|
1641
1932
|
ctx.cwd,
|
|
1642
1933
|
enqueueQueued,
|
|
1934
|
+
openOrchestrationRun,
|
|
1643
1935
|
pushMessage,
|
|
1644
1936
|
recordFeedback,
|
|
1645
1937
|
removeStoredCredential,
|
|
@@ -1899,9 +2191,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1899
2191
|
// A picker-owned provider/model change is one state transition. Clear the
|
|
1900
2192
|
// old model's effort selection before publishing the replacement session
|
|
1901
2193
|
// or releasing any paused queue. Failed and superseded candidates returned
|
|
1902
|
-
// above, so they leave the current session selection untouched.
|
|
1903
|
-
|
|
1904
|
-
|
|
2194
|
+
// above, so they leave the current session selection untouched. Orchestrate
|
|
2195
|
+
// mode survives the switch: re-pin to the new model's highest published
|
|
2196
|
+
// level instead of clearing, exactly as it pinned when first turned on.
|
|
2197
|
+
if (signal !== undefined) {
|
|
2198
|
+
setReasoningEffort(orchestrateModeRef.current ? highestReasoningEffort(s.reasoningEffortLevels) : undefined);
|
|
2199
|
+
}
|
|
1905
2200
|
// Re-hydration (a provider switch via /model) builds a second session;
|
|
1906
2201
|
// without this the first one's tool-server child processes stay alive
|
|
1907
2202
|
// for the rest of the TUI's life.
|
|
@@ -1909,7 +2204,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1909
2204
|
void previousSessionRef.current?.close();
|
|
1910
2205
|
previousSessionRef.current = s;
|
|
1911
2206
|
setSession(s);
|
|
1912
|
-
|
|
2207
|
+
// Orchestrate mode already re-pinned effort above; reopening this picker
|
|
2208
|
+
// would ask the operator to redo a choice the mode just made for them.
|
|
2209
|
+
if (!orchestrateModeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
|
|
1913
2210
|
// Own input before publishing ready or releasing a paused queue. The
|
|
1914
2211
|
// menu closes only after choosing an effort or keeping the new default.
|
|
1915
2212
|
setSelectedChoice(0);
|
|
@@ -2597,6 +2894,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2597
2894
|
: null;
|
|
2598
2895
|
pendingGoalResumeRef.current = conv.id;
|
|
2599
2896
|
scope.sessionId = conv.id; // new turns now attribute to the resumed session
|
|
2897
|
+
// The scope now names the resumed conversation, so its finished
|
|
2898
|
+
// children are readable. Without this the first Ctrl+T after a resume
|
|
2899
|
+
// would have nothing to find.
|
|
2900
|
+
void hydrateSavedChildren();
|
|
2600
2901
|
conversationMaterializedRef.current = true;
|
|
2601
2902
|
pushMessage('system', `Resumed: ${conv.title}`);
|
|
2602
2903
|
if (discardedQueued > 0) {
|
|
@@ -2617,6 +2918,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2617
2918
|
nextId,
|
|
2618
2919
|
pushMessage,
|
|
2619
2920
|
cancelPendingModelSwitch,
|
|
2921
|
+
hydrateSavedChildren,
|
|
2620
2922
|
resetSubagentActivity,
|
|
2621
2923
|
session,
|
|
2622
2924
|
resetTranscript,
|
|
@@ -2668,6 +2970,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2668
2970
|
if (sourceScope && targetSessionId) {
|
|
2669
2971
|
sourceScope.sessionId = targetSessionId;
|
|
2670
2972
|
conversationMaterializedRef.current = true;
|
|
2973
|
+
// A fresh conversation owns no runs yet, so this normally reads
|
|
2974
|
+
// nothing. It runs anyway because the rule is "the scope changed,
|
|
2975
|
+
// re-read", and an exception here is how the resume path lost
|
|
2976
|
+
// its own re-read.
|
|
2977
|
+
void hydrateSavedChildren();
|
|
2671
2978
|
}
|
|
2672
2979
|
if (clearScreen) {
|
|
2673
2980
|
setMessages([]);
|
|
@@ -2700,6 +3007,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2700
3007
|
interruptTurn,
|
|
2701
3008
|
pushMessage,
|
|
2702
3009
|
cancelPendingModelSwitch,
|
|
3010
|
+
hydrateSavedChildren,
|
|
2703
3011
|
resetSubagentActivity,
|
|
2704
3012
|
session,
|
|
2705
3013
|
resetTranscript,
|
|
@@ -2852,6 +3160,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2852
3160
|
// is reloaded or reset. Only where the NEXT turn is written changes.
|
|
2853
3161
|
scope.sessionId = forked.id;
|
|
2854
3162
|
resetSubagentActivity();
|
|
3163
|
+
void hydrateSavedChildren();
|
|
2855
3164
|
session?.resetTaskStore?.();
|
|
2856
3165
|
goalActivation.clear();
|
|
2857
3166
|
wakeGoalDriver();
|
|
@@ -2869,6 +3178,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2869
3178
|
hasUnsettledTurn,
|
|
2870
3179
|
materializeConversation,
|
|
2871
3180
|
pushMessage,
|
|
3181
|
+
hydrateSavedChildren,
|
|
2872
3182
|
resetSubagentActivity,
|
|
2873
3183
|
session,
|
|
2874
3184
|
wakeGoalDriver,
|
|
@@ -2962,6 +3272,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2962
3272
|
goalActivation.clear();
|
|
2963
3273
|
wakeGoalDriver();
|
|
2964
3274
|
scope.sessionId = forked.id;
|
|
3275
|
+
void hydrateSavedChildren();
|
|
2965
3276
|
modelHistoryRef.current = forked.messages;
|
|
2966
3277
|
lastAssistantMessage.current = null;
|
|
2967
3278
|
const persistedOutput = latestAssistantOutput(forked.messages);
|
|
@@ -2999,6 +3310,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2999
3310
|
nextId,
|
|
3000
3311
|
pushMessage,
|
|
3001
3312
|
cancelPendingModelSwitch,
|
|
3313
|
+
hydrateSavedChildren,
|
|
3002
3314
|
resetSubagentActivity,
|
|
3003
3315
|
session,
|
|
3004
3316
|
resetTranscript,
|
|
@@ -3223,6 +3535,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3223
3535
|
event.toolName.toLowerCase() === 'agent' &&
|
|
3224
3536
|
subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
|
|
3225
3537
|
(event.runId === undefined || agent.workflowId === event.runId));
|
|
3538
|
+
// The band above the rail already carries a narrated line, in the
|
|
3539
|
+
// parent's own words, from the moment it is written. Printing the
|
|
3540
|
+
// call that wrote it would show the same sentence twice, the
|
|
3541
|
+
// second time as protocol — the same duplicate-protocol case as
|
|
3542
|
+
// the represented Agent call just below, and refused the same
|
|
3543
|
+
// way. A REFUSED line keeps its row: nothing reached the screen,
|
|
3544
|
+
// and that row is the only thing that says so.
|
|
3545
|
+
if (!event.isError && event.toolName === NARRATION_TOOL_NAME) {
|
|
3546
|
+
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
3547
|
+
break;
|
|
3548
|
+
}
|
|
3226
3549
|
if (representedSuccessfulAgent) {
|
|
3227
3550
|
// The automatic panel already owned this successful child from
|
|
3228
3551
|
// start through settlement. Re-emitting its internal Agent call as
|
|
@@ -3678,6 +4001,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3678
4001
|
activeTurnInboxRef.current = inbox;
|
|
3679
4002
|
const turnPermissionMode = permissionModeRef.current;
|
|
3680
4003
|
const turnReasoningEffort = reasoningEffortRef.current;
|
|
4004
|
+
const turnOrchestrateMode = orchestrateModeRef.current;
|
|
3681
4005
|
const turnRunLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
|
|
3682
4006
|
// Always carry the guarded callback. `auto` and `strict` decide before
|
|
3683
4007
|
// calling it in makeResumeHandler; retaining it is what lets a session
|
|
@@ -3738,6 +4062,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3738
4062
|
permissionMode: turnPermissionMode,
|
|
3739
4063
|
limits: turnRunLimits,
|
|
3740
4064
|
...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
|
|
4065
|
+
...(turnOrchestrateMode ? { orchestrate: true } : {}),
|
|
3741
4066
|
...(goalRound ? { goalRound } : {}),
|
|
3742
4067
|
// The mode above decides whether this callback is consulted.
|
|
3743
4068
|
onPermission: askPermission,
|
|
@@ -4218,9 +4543,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4218
4543
|
return;
|
|
4219
4544
|
}
|
|
4220
4545
|
case 'agent-cockpit': {
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4546
|
+
// Reported after the read, not before it: the answer to "are
|
|
4547
|
+
// there any" may be on disk, and a conversation whose evidence
|
|
4548
|
+
// is sitting in its own directory must never be told it
|
|
4549
|
+
// delegated nothing.
|
|
4550
|
+
const agentIds = session?.agentIds ?? [];
|
|
4551
|
+
void openAgentCockpit().then((opened) => {
|
|
4552
|
+
if (opened)
|
|
4553
|
+
return;
|
|
4554
|
+
pushMessage('system', `No delegated agents in this conversation.${agentIds.length ? `\nAvailable: ${agentIds.join(', ')}` : '\nNo agents are configured.'}`);
|
|
4555
|
+
});
|
|
4556
|
+
return;
|
|
4557
|
+
}
|
|
4558
|
+
case 'agent-runs': {
|
|
4559
|
+
openAgentRunsPicker();
|
|
4224
4560
|
return;
|
|
4225
4561
|
}
|
|
4226
4562
|
case 'provider-setup':
|
|
@@ -4557,7 +4893,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4557
4893
|
const values = [undefined, ...levels];
|
|
4558
4894
|
const current = reasoningEffortRef.current;
|
|
4559
4895
|
setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
|
|
4560
|
-
setChoicePicker(reasoningEffortPicker(session, current) ?? null);
|
|
4896
|
+
setChoicePicker(reasoningEffortPicker(session, current, false, orchestrateModeRef.current) ?? null);
|
|
4561
4897
|
return;
|
|
4562
4898
|
}
|
|
4563
4899
|
case 'login':
|
|
@@ -4975,6 +5311,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4975
5311
|
: 'Raw output mode off — rich transcript rendering restored.');
|
|
4976
5312
|
return;
|
|
4977
5313
|
}
|
|
5314
|
+
case 'orchestrate-mode': {
|
|
5315
|
+
const enabled = slash.enabled === 'toggle' ? !orchestrateModeRef.current : slash.enabled;
|
|
5316
|
+
applyOrchestrateMode(enabled);
|
|
5317
|
+
return;
|
|
5318
|
+
}
|
|
4978
5319
|
case 'export-picker': {
|
|
4979
5320
|
if (!stableExportSource())
|
|
4980
5321
|
return;
|
|
@@ -5057,6 +5398,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5057
5398
|
hostCommands,
|
|
5058
5399
|
nextId,
|
|
5059
5400
|
openAgentCockpit,
|
|
5401
|
+
openAgentRunsPicker,
|
|
5060
5402
|
pushMessage,
|
|
5061
5403
|
rawOutput,
|
|
5062
5404
|
removeStoredCredential,
|
|
@@ -5557,8 +5899,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5557
5899
|
setAgentSurface(null);
|
|
5558
5900
|
return;
|
|
5559
5901
|
}
|
|
5560
|
-
if (
|
|
5902
|
+
if (openCockpitFromCurrentRows()) {
|
|
5903
|
+
void hydrateSavedChildren();
|
|
5561
5904
|
return;
|
|
5905
|
+
}
|
|
5906
|
+
// Nothing on screen yet. The conversation's children may exist only
|
|
5907
|
+
// as saved evidence, so the request outlives that read rather than
|
|
5908
|
+
// being answered by an empty snapshot.
|
|
5909
|
+
void openAgentCockpit();
|
|
5562
5910
|
}
|
|
5563
5911
|
// Child observation owns navigation, but never outranks consent: the
|
|
5564
5912
|
// permission branch above preempts it if a child or parent call asks.
|
|
@@ -5696,7 +6044,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5696
6044
|
return;
|
|
5697
6045
|
}
|
|
5698
6046
|
if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
|
|
5699
|
-
|
|
6047
|
+
// Reserving the replayed banner's row here too keeps one PgDn a
|
|
6048
|
+
// screenful of what is actually visible.
|
|
6049
|
+
const page = agentTranscriptPageSize(terminal.rows, selected.replayed ? 1 : 0);
|
|
5700
6050
|
const max = maxAgentTranscriptTailOffset(selected, terminal.rows, terminal.columns);
|
|
5701
6051
|
const offset = Math.min(agentView.tailOffset, max);
|
|
5702
6052
|
const next = key.home
|
|
@@ -5970,6 +6320,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5970
6320
|
? permissionOwner ? `Agent: ${permissionOwner.description}` : `Run: ${permission.runId}`
|
|
5971
6321
|
: undefined;
|
|
5972
6322
|
const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
|
|
6323
|
+
// True exactly when the phase ternary below renders its final branch — the
|
|
6324
|
+
// normal composer view, as opposed to provider setup or a lifecycle
|
|
6325
|
+
// picker/prompt. The agent rail is a feature of that view: computing this
|
|
6326
|
+
// once here lets its render site sit after the footer, below, while still
|
|
6327
|
+
// never appearing during trust/resume/edit/picker/setup phases.
|
|
6328
|
+
const showComposerSurface = !providerSetup &&
|
|
6329
|
+
phase !== 'trust' &&
|
|
6330
|
+
phase !== 'unhealthy' &&
|
|
6331
|
+
phase !== 'resume' &&
|
|
6332
|
+
phase !== 'edit' &&
|
|
6333
|
+
phase !== 'picker';
|
|
5973
6334
|
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: () => {
|
|
5974
6335
|
setProviderSetup(false);
|
|
5975
6336
|
setPickerInitialView('providers');
|
|
@@ -6003,7 +6364,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6003
6364
|
? queuePause.outcome === 'paused'
|
|
6004
6365
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
6005
6366
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
6006
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels,
|
|
6367
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
6007
6368
|
phase !== 'ready' ||
|
|
6008
6369
|
state === 'awaiting-permission' ||
|
|
6009
6370
|
compacting ||
|
|
@@ -6020,13 +6381,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6020
6381
|
(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) => ({
|
|
6021
6382
|
...command,
|
|
6022
6383
|
description: command.unavailable?.(slashCtx) ?? command.description,
|
|
6023
|
-
})), mentionCandidates: mentionCandidates, history: history })] }),
|
|
6384
|
+
})), 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 &&
|
|
6385
|
+
permission === null &&
|
|
6386
|
+
agentSurface === null &&
|
|
6387
|
+
outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
|
|
6388
|
+
permission === null &&
|
|
6389
|
+
agentSurface === null &&
|
|
6390
|
+
outputViewer === null &&
|
|
6391
|
+
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 &&
|
|
6392
|
+
permission === null &&
|
|
6393
|
+
agentSurface?.kind === 'transcript' &&
|
|
6394
|
+
selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
|
|
6024
6395
|
if (outputIndex > 0)
|
|
6025
6396
|
setOutputViewer(outputBlocks[outputIndex - 1]);
|
|
6026
6397
|
}, onNext: () => {
|
|
6027
6398
|
if (outputIndex >= 0 && outputIndex + 1 < outputBlocks.length)
|
|
6028
6399
|
setOutputViewer(outputBlocks[outputIndex + 1]);
|
|
6029
|
-
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null
|
|
6400
|
+
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null] }) }));
|
|
6030
6401
|
}
|
|
6031
6402
|
/** One line for a background job that ended, for the transcript and the next turn. */
|
|
6032
6403
|
function describeJobExit(job) {
|
|
@@ -6103,6 +6474,33 @@ function goalStatusLabel(goal, armed) {
|
|
|
6103
6474
|
function labelOfOption(question, optionId) {
|
|
6104
6475
|
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
6105
6476
|
}
|
|
6477
|
+
/** `/agents runs`' one-line-per-run text, mirroring `/jobs`' own column shape. */
|
|
6478
|
+
function agentRunOption(run) {
|
|
6479
|
+
const agents = `${run.agentsDone}/${run.agentsTotal} agent${run.agentsTotal === 1 ? '' : 's'}`;
|
|
6480
|
+
const tokens = `${run.tokensTotal.toLocaleString('en-US')} tokens`;
|
|
6481
|
+
const columns = [
|
|
6482
|
+
relativeRunStart(run.startedAt),
|
|
6483
|
+
run.phases.join(', '),
|
|
6484
|
+
agents,
|
|
6485
|
+
tokens,
|
|
6486
|
+
formatElapsed(run.elapsedMs),
|
|
6487
|
+
];
|
|
6488
|
+
if (run.live)
|
|
6489
|
+
columns.push('live');
|
|
6490
|
+
return { label: run.name, description: columns.join(' · ') };
|
|
6491
|
+
}
|
|
6492
|
+
/** `just now` / `12m ago` / `3h ago` / `2d ago` — the same buckets a conversation's own resume picker shows. */
|
|
6493
|
+
function relativeRunStart(startedAt) {
|
|
6494
|
+
const minutes = Math.round((Date.now() - startedAt) / 60_000);
|
|
6495
|
+
if (minutes < 1)
|
|
6496
|
+
return 'just now';
|
|
6497
|
+
if (minutes < 60)
|
|
6498
|
+
return `${minutes}m ago`;
|
|
6499
|
+
const hours = Math.round(minutes / 60);
|
|
6500
|
+
if (hours < 24)
|
|
6501
|
+
return `${hours}h ago`;
|
|
6502
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
6503
|
+
}
|
|
6106
6504
|
function choicePickerSearchable(picker) {
|
|
6107
6505
|
return ['command', 'skill', 'plugin', 'review-branch', 'review-commit'].includes(picker.kind);
|
|
6108
6506
|
}
|
|
@@ -6184,11 +6582,27 @@ function permissionPicker(mode, approvedAll, more = false) {
|
|
|
6184
6582
|
}),
|
|
6185
6583
|
};
|
|
6186
6584
|
}
|
|
6187
|
-
|
|
6585
|
+
/**
|
|
6586
|
+
* The picker's sentinel for the orchestrate-mode row, below the rule.
|
|
6587
|
+
*
|
|
6588
|
+
* A plain string outside the `ReasoningEffort` union by construction — never
|
|
6589
|
+
* added to any model's published menu — so it can share the same picker
|
|
6590
|
+
* without ever being mistaken for a level `/effort <token>` could select.
|
|
6591
|
+
*/
|
|
6592
|
+
const ORCHESTRATE_MODE_VALUE = 'orchestrate';
|
|
6593
|
+
/** Last entry of a low-to-high menu every provider publishes in that order; `undefined` when none is known. */
|
|
6594
|
+
function highestReasoningEffort(levels) {
|
|
6595
|
+
return levels && levels.length > 0 ? levels[levels.length - 1] : undefined;
|
|
6596
|
+
}
|
|
6597
|
+
function reasoningEffortPicker(session, current, afterModelSelection = false, orchestrateOn = false) {
|
|
6188
6598
|
const levels = session.reasoningEffortLevels;
|
|
6189
6599
|
if (!session.hasProvider || levels === undefined)
|
|
6190
6600
|
return undefined;
|
|
6191
|
-
const
|
|
6601
|
+
const effortValues = [undefined, ...levels];
|
|
6602
|
+
const values = [
|
|
6603
|
+
...effortValues,
|
|
6604
|
+
ORCHESTRATE_MODE_VALUE,
|
|
6605
|
+
];
|
|
6192
6606
|
return {
|
|
6193
6607
|
kind: 'reasoning-effort',
|
|
6194
6608
|
title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
|
|
@@ -6199,12 +6613,24 @@ function reasoningEffortPicker(session, current, afterModelSelection = false) {
|
|
|
6199
6613
|
}
|
|
6200
6614
|
: {}),
|
|
6201
6615
|
values,
|
|
6202
|
-
options:
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6616
|
+
options: [
|
|
6617
|
+
...effortValues.map((effort) => ({
|
|
6618
|
+
label: effort ?? 'default',
|
|
6619
|
+
description: reasoningEffortDescription(effort),
|
|
6620
|
+
current: effort === current,
|
|
6621
|
+
default: effort === undefined,
|
|
6622
|
+
})),
|
|
6623
|
+
{
|
|
6624
|
+
// Below a rule, visually apart from the levels above: a session
|
|
6625
|
+
// setting, not a sixth level the provider published.
|
|
6626
|
+
label: 'orchestrate',
|
|
6627
|
+
description: orchestrateOn
|
|
6628
|
+
? 'On · highest level and delegate by default'
|
|
6629
|
+
: 'Off · highest level and delegate by default',
|
|
6630
|
+
current: orchestrateOn,
|
|
6631
|
+
ruleBefore: true,
|
|
6632
|
+
},
|
|
6633
|
+
],
|
|
6208
6634
|
};
|
|
6209
6635
|
}
|
|
6210
6636
|
function reasoningEffortDescription(effort) {
|