@namzu/cli 15.1.2 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/state.d.ts +9 -0
- package/dist/commands/state.d.ts.map +1 -0
- package/dist/commands/state.js +110 -0
- package/dist/commands/state.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +8 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/capabilities.d.ts +7 -4
- package/dist/context/capabilities.d.ts.map +1 -1
- package/dist/context/capabilities.js +7 -4
- package/dist/context/capabilities.js.map +1 -1
- package/dist/context/sandbox.d.ts +5 -0
- package/dist/context/sandbox.d.ts.map +1 -1
- package/dist/context/sandbox.js +11 -4
- package/dist/context/sandbox.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +25 -2
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/state/private-directory.d.ts +15 -0
- package/dist/integrations/state/private-directory.d.ts.map +1 -0
- package/dist/integrations/state/private-directory.js +41 -0
- package/dist/integrations/state/private-directory.js.map +1 -0
- package/dist/integrations/state/report.d.ts +125 -0
- package/dist/integrations/state/report.d.ts.map +1 -0
- package/dist/integrations/state/report.js +784 -0
- package/dist/integrations/state/report.js.map +1 -0
- package/dist/integrations/subagents/activity.d.ts +88 -0
- package/dist/integrations/subagents/activity.d.ts.map +1 -0
- package/dist/integrations/subagents/activity.js +389 -0
- package/dist/integrations/subagents/activity.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +11 -1
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +66 -3
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/tui/AgentExplorer.d.ts +57 -0
- package/dist/tui/AgentExplorer.d.ts.map +1 -0
- package/dist/tui/AgentExplorer.js +285 -0
- package/dist/tui/AgentExplorer.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +479 -150
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Composer.d.ts +7 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +26 -5
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +9 -3
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +27 -10
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +8 -9
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +6 -10
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/StatusBar.d.ts +1 -47
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +9 -79
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/agent.d.ts +3 -0
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +217 -178
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/permission-review.d.ts +18 -2
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +88 -2
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +4 -0
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +14 -0
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/user-commands/store.d.ts.map +1 -1
- package/dist/user-commands/store.js +26 -5
- package/dist/user-commands/store.js.map +1 -1
- package/package.json +2 -2
package/dist/tui/App.js
CHANGED
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
* automatic reuse is not mistaken for a durable operator preference.
|
|
13
13
|
*/
|
|
14
14
|
import { join, relative } from 'node:path';
|
|
15
|
-
import { DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
15
|
+
import { DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
16
16
|
import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
18
|
import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
|
|
@@ -28,19 +28,18 @@ import { checkUpdates } from '../integrations/updates.js';
|
|
|
28
28
|
import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
29
29
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
30
30
|
import { discoverUserCommands } from '../user-commands/store.js';
|
|
31
|
+
import { AgentCockpit, AgentTranscript, agentCockpitIsWide, agentPickerPageSize, agentPhasePageSize, agentPhases, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
31
32
|
import { ChoicePicker } from './ChoicePicker.js';
|
|
32
33
|
import { Composer, suggestionWindowSize, } from './Composer.js';
|
|
33
34
|
import { CopyPicker } from './CopyPicker.js';
|
|
34
35
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
35
36
|
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
36
37
|
import { PermissionOverlay } from './PermissionOverlay.js';
|
|
37
|
-
import { PERMISSION_REVIEW_PAGE_ROWS, buildPermissionReview, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
38
38
|
import { Picker } from './Picker.js';
|
|
39
39
|
import { ResumePicker } from './ResumePicker.js';
|
|
40
40
|
import { StatusBar } from './StatusBar.js';
|
|
41
41
|
import { TextPrompt } from './TextPrompt.js';
|
|
42
42
|
import { Transcript, willCollapse } from './Transcript.js';
|
|
43
|
-
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
44
43
|
import { TrustPrompt } from './TrustPrompt.js';
|
|
45
44
|
import { createAgentSession, probeAgentSession, } from './agent.js';
|
|
46
45
|
import { keepRecentRows } from './compact-transcript.js';
|
|
@@ -48,18 +47,20 @@ import { approvalIsDeliberate } from './consent-timing.js';
|
|
|
48
47
|
import { planTurnPublication } from './conversation-history.js';
|
|
49
48
|
import { copyTargetsForResponse } from './copy-targets.js';
|
|
50
49
|
import { editablePrompts } from './edit-prompts.js';
|
|
50
|
+
import { editDraftInExternalEditor } from './external-editor.js';
|
|
51
51
|
import { liveWindow } from './live-window.js';
|
|
52
|
-
import { describeRunInterruption } from './run-interruption.js';
|
|
53
52
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
|
|
54
53
|
import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
|
|
55
54
|
import { expandFileMentions, listMentionableFiles } from './mentions.js';
|
|
56
55
|
import { openInBrowser } from './open-browser.js';
|
|
56
|
+
import { PERMISSION_REVIEW_PAGE_ROWS, buildPermissionReview, buildPermissionSummary, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
57
|
+
import { describeRunInterruption } from './run-interruption.js';
|
|
58
|
+
import { moveSelection } from './selection-window.js';
|
|
57
59
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
58
60
|
import { splitCompleteBlocks } from './stream-blocks.js';
|
|
59
|
-
import {
|
|
61
|
+
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
60
62
|
import { theme } from './theme.js';
|
|
61
63
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
62
|
-
import { editDraftInExternalEditor } from './external-editor.js';
|
|
63
64
|
import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
|
|
64
65
|
import { listReviewBranches, listReviewCommits, reviewMergeBase, } from './workspace-review.js';
|
|
65
66
|
/** Last non-empty assistant text in a durable conversation, newest first. */
|
|
@@ -293,6 +294,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
293
294
|
const [permission, setPermission] = useState(null);
|
|
294
295
|
const [permissionReviewOffset, setPermissionReviewOffsetState] = useState(0);
|
|
295
296
|
const permissionReviewOffsetRef = useRef(0);
|
|
297
|
+
const [permissionDetailsOpen, setPermissionDetailsOpenState] = useState(false);
|
|
298
|
+
const permissionDetailsOpenRef = useRef(false);
|
|
299
|
+
const setPermissionDetailsOpen = useCallback((next) => {
|
|
300
|
+
permissionDetailsOpenRef.current = next;
|
|
301
|
+
setPermissionDetailsOpenState(next);
|
|
302
|
+
}, []);
|
|
296
303
|
const setPermissionReviewOffset = useCallback((next) => {
|
|
297
304
|
permissionReviewOffsetRef.current = next;
|
|
298
305
|
setPermissionReviewOffsetState(next);
|
|
@@ -319,10 +326,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
319
326
|
const [mentionCandidates, setMentionCandidates] = useState([]);
|
|
320
327
|
const mentionLoadOwnerRef = useRef(null);
|
|
321
328
|
const [usage, setUsage] = useState(null);
|
|
322
|
-
// Context fill, straight from the kernel and held apart from `usage` —
|
|
323
|
-
// they are different quantities and conflating them is what made the
|
|
324
|
-
// gauge climb with turn count instead of with context.
|
|
325
|
-
const [context, setContext] = useState(null);
|
|
326
329
|
// Tools currently executing — rendered live (spinner + elapsed) below the
|
|
327
330
|
// transcript, then committed as static lines on completion.
|
|
328
331
|
const [activeTools, setActiveTools] = useState([]);
|
|
@@ -392,11 +395,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
392
395
|
/** `/copy` owns this exact response snapshot until selection or cancellation. */
|
|
393
396
|
const [copyPicker, setCopyPickerState] = useState(null);
|
|
394
397
|
const copyPickerRef = useRef(null);
|
|
398
|
+
const copyPickerCommittedRef = useRef(null);
|
|
395
399
|
const { selection: selectedCopy, selectionRef: selectedCopyRef, setSelection: setSelectedCopy, } = useSelectionIndex(0);
|
|
396
400
|
const setCopyPicker = useCallback((next) => {
|
|
397
401
|
copyPickerRef.current = next;
|
|
402
|
+
if (next === null)
|
|
403
|
+
copyPickerCommittedRef.current = null;
|
|
398
404
|
setCopyPickerState(next);
|
|
399
405
|
}, []);
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
copyPickerCommittedRef.current = copyPicker;
|
|
408
|
+
}, [copyPicker]);
|
|
400
409
|
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
401
410
|
const [choicePicker, setChoicePickerState] = useState(null);
|
|
402
411
|
const choicePickerRef = useRef(null);
|
|
@@ -426,7 +435,49 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
426
435
|
setTextPromptState(next);
|
|
427
436
|
}, []);
|
|
428
437
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
438
|
+
const [composerHasDraft, setComposerHasDraft] = useState(false);
|
|
429
439
|
const composerDraftTokenRef = useRef(0);
|
|
440
|
+
/** Bounded child-run projection published by the current AgentSession. */
|
|
441
|
+
const [subagents, setSubagentsState] = useState([]);
|
|
442
|
+
const subagentsRef = useRef([]);
|
|
443
|
+
const [agentSurface, setAgentSurfaceState] = useState(null);
|
|
444
|
+
const agentSurfaceRef = useRef(null);
|
|
445
|
+
const agentSurfaceCommittedRef = useRef(null);
|
|
446
|
+
const setAgentSurface = useCallback((next) => {
|
|
447
|
+
const wasVisible = agentSurfaceCommittedRef.current !== null;
|
|
448
|
+
agentSurfaceRef.current = next;
|
|
449
|
+
if (next === null)
|
|
450
|
+
agentSurfaceCommittedRef.current = null;
|
|
451
|
+
// Only opening the surface needs a paint fence. Once the operator has
|
|
452
|
+
// seen it, navigation may update the state object several times in one
|
|
453
|
+
// terminal input burst; those updates remain actionable immediately.
|
|
454
|
+
else if (wasVisible)
|
|
455
|
+
agentSurfaceCommittedRef.current = next;
|
|
456
|
+
setAgentSurfaceState(next);
|
|
457
|
+
}, []);
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
agentSurfaceCommittedRef.current = agentSurface;
|
|
460
|
+
}, [agentSurface]);
|
|
461
|
+
const replaceSubagents = useCallback((next) => {
|
|
462
|
+
subagentsRef.current = next;
|
|
463
|
+
setSubagentsState(next);
|
|
464
|
+
const surface = agentSurfaceRef.current;
|
|
465
|
+
if (!surface || next.some((agent) => agent.viewId === surface.selectedId))
|
|
466
|
+
return;
|
|
467
|
+
const phases = agentPhases(next);
|
|
468
|
+
const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? phases[0];
|
|
469
|
+
const fallbackAgent = fallbackPhase?.agents[0];
|
|
470
|
+
if (!fallbackPhase || !fallbackAgent) {
|
|
471
|
+
setAgentSurface(null);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
setAgentSurface({
|
|
475
|
+
kind: 'cockpit',
|
|
476
|
+
selectedPhaseId: fallbackPhase.id,
|
|
477
|
+
selectedId: fallbackAgent.viewId,
|
|
478
|
+
focus: surface.kind === 'cockpit' ? surface.focus : surface.returnFocus,
|
|
479
|
+
});
|
|
480
|
+
}, [setAgentSurface]);
|
|
430
481
|
/** One git-backed review choice may be resolving while its visible picker remains authoritative. */
|
|
431
482
|
const reviewChoiceInFlightRef = useRef(null);
|
|
432
483
|
const exitArmedRef = useRef(false);
|
|
@@ -466,6 +517,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
466
517
|
* server's child process outlives the object that opened it.
|
|
467
518
|
*/
|
|
468
519
|
const previousSessionRef = useRef(null);
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
const source = session?.subagents;
|
|
522
|
+
setAgentSurface(null);
|
|
523
|
+
if (!source) {
|
|
524
|
+
replaceSubagents([]);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const refresh = () => replaceSubagents(source.getSnapshot());
|
|
528
|
+
refresh();
|
|
529
|
+
return source.subscribe(refresh);
|
|
530
|
+
}, [replaceSubagents, session, setAgentSurface]);
|
|
531
|
+
const resetSubagentActivity = useCallback(() => {
|
|
532
|
+
session?.subagents?.reset();
|
|
533
|
+
setAgentSurface(null);
|
|
534
|
+
replaceSubagents([]);
|
|
535
|
+
}, [replaceSubagents, session, setAgentSurface]);
|
|
469
536
|
// Source of truth for in-flight tools (the event loop runs across renders, so
|
|
470
537
|
// a ref avoids stale state); `activeTools` mirrors it for rendering.
|
|
471
538
|
const activeToolsRef = useRef([]);
|
|
@@ -496,9 +563,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
496
563
|
// turns attribute to the resumed conversation.
|
|
497
564
|
const sessionsRef = useRef(null);
|
|
498
565
|
const scopeRef = useRef(null);
|
|
566
|
+
/**
|
|
567
|
+
* A provider session can be ready before a person has started a conversation.
|
|
568
|
+
* The scope still needs a mutable session cursor, but its generated id is
|
|
569
|
+
* provisional until the first admitted prompt or an explicit conversation
|
|
570
|
+
* operation publishes the durable Session.
|
|
571
|
+
*/
|
|
572
|
+
const conversationMaterializedRef = useRef(false);
|
|
573
|
+
const materializationRef = useRef(null);
|
|
499
574
|
const exitWithSummary = useCallback(() => {
|
|
500
575
|
onExitSummary?.({
|
|
501
|
-
...(
|
|
576
|
+
...(conversationMaterializedRef.current && scopeRef.current?.sessionId
|
|
577
|
+
? { conversationId: scopeRef.current.sessionId }
|
|
578
|
+
: {}),
|
|
502
579
|
});
|
|
503
580
|
exit();
|
|
504
581
|
}, [exit, onExitSummary]);
|
|
@@ -775,12 +852,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
775
852
|
}
|
|
776
853
|
applyReasoningEffort(next);
|
|
777
854
|
}, [applyReasoningEffort, pushMessage, session]);
|
|
778
|
-
const recordFeedback = useCallback((runIdValue, messageIdValue, rating, note) => {
|
|
779
|
-
|
|
780
|
-
|
|
855
|
+
const recordFeedback = useCallback((sessionId, runIdValue, messageIdValue, rating, note) => {
|
|
856
|
+
const sessions = sessionsRef.current;
|
|
857
|
+
if (!sessions) {
|
|
858
|
+
pushMessage('system', 'Could not record feedback: conversation persistence is unavailable.');
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
// Run evidence is session-scoped in the canonical store. The old flat
|
|
862
|
+
// `<cwd>/.namzu/runs` lookup never existed after the hierarchy migration,
|
|
863
|
+
// so the feedback store correctly refused every TUI rating as unverifiable.
|
|
864
|
+
// Bind both trees to the exact conversation captured with the streamed
|
|
865
|
+
// message; `/resume` may change the active scope before a delayed click.
|
|
866
|
+
const sessionDir = new DefaultPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
|
|
781
867
|
const store = new DiskMessageFeedbackStore({
|
|
782
|
-
rootDir: join(
|
|
783
|
-
runsDir: join(
|
|
868
|
+
rootDir: join(sessionDir, 'feedback'),
|
|
869
|
+
runsDir: join(sessionDir, 'runs'),
|
|
784
870
|
});
|
|
785
871
|
const runId = runIdValue;
|
|
786
872
|
const messageId = messageIdValue;
|
|
@@ -802,7 +888,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
802
888
|
pushMessage('system', `Could not record feedback: ${error instanceof Error ? error.message : String(error)}`);
|
|
803
889
|
}
|
|
804
890
|
})();
|
|
805
|
-
}, [
|
|
891
|
+
}, [pushMessage]);
|
|
806
892
|
const activateSkill = useCallback((name) => {
|
|
807
893
|
const info = discoverSkills({ cwd: ctx.cwd }).find((skill) => skill.name === name);
|
|
808
894
|
if (!info) {
|
|
@@ -849,8 +935,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
849
935
|
return;
|
|
850
936
|
const sessions = sessionsRef.current;
|
|
851
937
|
const sessionId = scopeRef.current?.sessionId;
|
|
852
|
-
if (!sessions || !sessionId) {
|
|
853
|
-
pushMessage('system',
|
|
938
|
+
if (!sessions || !sessionId || !conversationMaterializedRef.current) {
|
|
939
|
+
pushMessage('system', !conversationMaterializedRef.current
|
|
940
|
+
? 'There is no conversation to archive yet — send a message first.'
|
|
941
|
+
: 'Cannot archive this conversation because durable session persistence is unavailable.');
|
|
854
942
|
return;
|
|
855
943
|
}
|
|
856
944
|
if (state !== 'idle' ||
|
|
@@ -901,8 +989,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
901
989
|
const stableExportSource = useCallback(() => {
|
|
902
990
|
const sessions = sessionsRef.current;
|
|
903
991
|
const sessionId = scopeRef.current?.sessionId;
|
|
904
|
-
if (!sessions || !sessionId) {
|
|
905
|
-
pushMessage('system',
|
|
992
|
+
if (!sessions || !sessionId || !conversationMaterializedRef.current) {
|
|
993
|
+
pushMessage('system', !conversationMaterializedRef.current
|
|
994
|
+
? 'There is no conversation to export yet — send a message first.'
|
|
995
|
+
: 'Cannot export this conversation because durable session persistence is unavailable.');
|
|
906
996
|
return null;
|
|
907
997
|
}
|
|
908
998
|
if (abortRef.current ||
|
|
@@ -1149,7 +1239,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1149
1239
|
return;
|
|
1150
1240
|
}
|
|
1151
1241
|
if (picker.kind === 'feedback-rating') {
|
|
1152
|
-
recordFeedback(picker.runId, picker.messageId, value);
|
|
1242
|
+
recordFeedback(picker.sessionId, picker.runId, picker.messageId, value);
|
|
1153
1243
|
return;
|
|
1154
1244
|
}
|
|
1155
1245
|
if (picker.kind === 'skill') {
|
|
@@ -1268,9 +1358,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1268
1358
|
? { text: persistedOutput, provenance: 'persisted' }
|
|
1269
1359
|
: null;
|
|
1270
1360
|
initialConversationIdRef.current = undefined;
|
|
1361
|
+
conversationMaterializedRef.current = true;
|
|
1271
1362
|
}
|
|
1272
1363
|
else {
|
|
1273
|
-
|
|
1364
|
+
// Provider/tool startup needs stable project/tenant/topic ids, not a
|
|
1365
|
+
// durable conversation. The cursor is replaced on first admitted use.
|
|
1366
|
+
sessionId = generateSessionId();
|
|
1274
1367
|
}
|
|
1275
1368
|
sessionsRef.current = sessions;
|
|
1276
1369
|
scopeRef.current = {
|
|
@@ -1287,6 +1380,45 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1287
1380
|
return undefined;
|
|
1288
1381
|
}
|
|
1289
1382
|
}, [nextId]);
|
|
1383
|
+
/** Publish the provisional conversation exactly once, at first durable use. */
|
|
1384
|
+
const materializeConversation = useCallback(async () => {
|
|
1385
|
+
const existing = await ensureSessions();
|
|
1386
|
+
if (!existing)
|
|
1387
|
+
return undefined;
|
|
1388
|
+
if (conversationMaterializedRef.current)
|
|
1389
|
+
return existing;
|
|
1390
|
+
if (materializationRef.current)
|
|
1391
|
+
return materializationRef.current;
|
|
1392
|
+
const pending = (async () => {
|
|
1393
|
+
const sessions = sessionsRef.current;
|
|
1394
|
+
const scope = scopeRef.current;
|
|
1395
|
+
if (!sessions || !scope)
|
|
1396
|
+
return undefined;
|
|
1397
|
+
conversationMutationRef.current = 'materialize';
|
|
1398
|
+
try {
|
|
1399
|
+
const sessionId = await startConversation(sessions);
|
|
1400
|
+
if (scopeRef.current !== scope || conversationMaterializedRef.current) {
|
|
1401
|
+
throw new Error('the active conversation changed while its first session was published');
|
|
1402
|
+
}
|
|
1403
|
+
scope.sessionId = sessionId;
|
|
1404
|
+
conversationMaterializedRef.current = true;
|
|
1405
|
+
return scope;
|
|
1406
|
+
}
|
|
1407
|
+
finally {
|
|
1408
|
+
if (conversationMutationRef.current === 'materialize') {
|
|
1409
|
+
conversationMutationRef.current = null;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
})();
|
|
1413
|
+
materializationRef.current = pending;
|
|
1414
|
+
try {
|
|
1415
|
+
return await pending;
|
|
1416
|
+
}
|
|
1417
|
+
finally {
|
|
1418
|
+
if (materializationRef.current === pending)
|
|
1419
|
+
materializationRef.current = null;
|
|
1420
|
+
}
|
|
1421
|
+
}, [ensureSessions]);
|
|
1290
1422
|
const hydrateSession = useCallback(async (prefs, detectedNow, signal) => {
|
|
1291
1423
|
if (signal?.aborted)
|
|
1292
1424
|
return;
|
|
@@ -1341,8 +1473,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1341
1473
|
const mentionLoadOwner = {};
|
|
1342
1474
|
mentionLoadOwnerRef.current = mentionLoadOwner;
|
|
1343
1475
|
void listMentionableFiles(activeCtx.cwd, appLifetime.signal).then((files) => {
|
|
1344
|
-
if (!appLifetime.signal.aborted &&
|
|
1345
|
-
mentionLoadOwnerRef.current === mentionLoadOwner)
|
|
1476
|
+
if (!appLifetime.signal.aborted && mentionLoadOwnerRef.current === mentionLoadOwner)
|
|
1346
1477
|
setMentionCandidates(files);
|
|
1347
1478
|
});
|
|
1348
1479
|
setCurrentProvider(primaryProvider(prefs).id);
|
|
@@ -1715,7 +1846,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1715
1846
|
settled: settledRef.current,
|
|
1716
1847
|
raw: rawOutput,
|
|
1717
1848
|
});
|
|
1718
|
-
|
|
1849
|
+
// A child view is a redrawable overlay above the parent's native scrollback.
|
|
1850
|
+
// Freeze the parent's Static floor while it is open; otherwise a parent turn
|
|
1851
|
+
// settling in the background prints through the child screen. Returning
|
|
1852
|
+
// advances the floor once and emits those finalized rows exactly once.
|
|
1853
|
+
if (agentSurface === null)
|
|
1854
|
+
settledRef.current = window.settled;
|
|
1855
|
+
const renderedSettled = agentSurface === null ? window.settled : settledRef.current;
|
|
1719
1856
|
// One merged vocabulary for the session: this host's own commands plus
|
|
1720
1857
|
// whatever the kernel's registry reports. Built here so `/help`, the
|
|
1721
1858
|
// autocomplete and the dispatcher all answer from the same list — three
|
|
@@ -1801,10 +1938,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1801
1938
|
permissionResolveRef.current = null;
|
|
1802
1939
|
permissionOpenedAtRef.current = null;
|
|
1803
1940
|
setPermissionReviewOffset(0);
|
|
1941
|
+
setPermissionDetailsOpen(false);
|
|
1804
1942
|
setPermission(null);
|
|
1805
1943
|
if (resolve)
|
|
1806
1944
|
resolve(decision);
|
|
1807
|
-
}, [setPermissionReviewOffset]);
|
|
1945
|
+
}, [setPermissionDetailsOpen, setPermissionReviewOffset]);
|
|
1808
1946
|
/**
|
|
1809
1947
|
* Stop the running turn, and hand the screen back in a usable state.
|
|
1810
1948
|
*
|
|
@@ -1918,16 +2056,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1918
2056
|
goalActivation.clear();
|
|
1919
2057
|
wakeGoalDriver();
|
|
1920
2058
|
conversationGenRef.current += 1;
|
|
2059
|
+
resetSubagentActivity();
|
|
1921
2060
|
activeTurnTokenRef.current = null;
|
|
1922
2061
|
resetTranscript();
|
|
1923
2062
|
setMessages(restored);
|
|
1924
2063
|
modelHistoryRef.current = msgs;
|
|
1925
2064
|
setHistory((previous) => [...previous, ...promptHistoryFromConversation(msgs)]);
|
|
2065
|
+
// Persisted conversation rows do not carry the live run/message identity
|
|
2066
|
+
// required by feedback validation. Do not leave the answer from the
|
|
2067
|
+
// conversation we just exited as a hidden `/feedback` target.
|
|
2068
|
+
lastAssistantMessage.current = null;
|
|
1926
2069
|
const persistedOutput = latestAssistantOutput(msgs);
|
|
1927
2070
|
lastCompletedOutputRef.current = persistedOutput
|
|
1928
2071
|
? { text: persistedOutput, provenance: 'persisted' }
|
|
1929
2072
|
: null;
|
|
1930
2073
|
scope.sessionId = conv.id; // new turns now attribute to the resumed session
|
|
2074
|
+
conversationMaterializedRef.current = true;
|
|
1931
2075
|
pushMessage('system', `Resumed: ${conv.title}`);
|
|
1932
2076
|
if (discardedQueued > 0) {
|
|
1933
2077
|
pushMessage('system', `Discarded ${discardedQueued} queued prompt${discardedQueued === 1 ? '' : 's'} from the conversation you left.`);
|
|
@@ -1946,6 +2090,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1946
2090
|
interruptTurn,
|
|
1947
2091
|
nextId,
|
|
1948
2092
|
pushMessage,
|
|
2093
|
+
resetSubagentActivity,
|
|
1949
2094
|
resetTranscript,
|
|
1950
2095
|
wakeGoalDriver,
|
|
1951
2096
|
]);
|
|
@@ -1985,13 +2130,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1985
2130
|
goalActivation.clear();
|
|
1986
2131
|
wakeGoalDriver();
|
|
1987
2132
|
conversationGenRef.current += 1;
|
|
2133
|
+
resetSubagentActivity();
|
|
1988
2134
|
activeTurnTokenRef.current = null;
|
|
1989
2135
|
modelHistoryRef.current = [];
|
|
1990
2136
|
lastAssistantMessage.current = null;
|
|
1991
2137
|
lastCompletedOutputRef.current = null;
|
|
1992
|
-
|
|
1993
|
-
if (sourceScope && targetSessionId)
|
|
2138
|
+
if (sourceScope && targetSessionId) {
|
|
1994
2139
|
sourceScope.sessionId = targetSessionId;
|
|
2140
|
+
conversationMaterializedRef.current = true;
|
|
2141
|
+
}
|
|
1995
2142
|
if (clearScreen) {
|
|
1996
2143
|
setMessages([]);
|
|
1997
2144
|
resetTranscript();
|
|
@@ -2017,7 +2164,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2017
2164
|
conversationMutationRef.current = null;
|
|
2018
2165
|
setConversationMutation(null);
|
|
2019
2166
|
}
|
|
2020
|
-
}, [
|
|
2167
|
+
}, [
|
|
2168
|
+
discardQueued,
|
|
2169
|
+
goalActivation,
|
|
2170
|
+
interruptTurn,
|
|
2171
|
+
pushMessage,
|
|
2172
|
+
resetSubagentActivity,
|
|
2173
|
+
resetTranscript,
|
|
2174
|
+
wakeGoalDriver,
|
|
2175
|
+
]);
|
|
2021
2176
|
/**
|
|
2022
2177
|
* `/rename` (and the older `/title` alias): edit or set the name this
|
|
2023
2178
|
* conversation appears under.
|
|
@@ -2027,8 +2182,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2027
2182
|
* history; it is a session-store operation with the session id captured here.
|
|
2028
2183
|
*/
|
|
2029
2184
|
const doTitle = useCallback(async (title, clear) => {
|
|
2030
|
-
const
|
|
2031
|
-
const
|
|
2185
|
+
const scope = await materializeConversation();
|
|
2186
|
+
const sessions = sessionsRef.current;
|
|
2032
2187
|
if (!sessions || !scope) {
|
|
2033
2188
|
pushMessage('system', 'Conversation history is unavailable in this folder.');
|
|
2034
2189
|
return;
|
|
@@ -2056,7 +2211,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2056
2211
|
catch (err) {
|
|
2057
2212
|
pushMessage('system', `Could not save the name: ${err instanceof Error ? err.message : String(err)}`);
|
|
2058
2213
|
}
|
|
2059
|
-
}, [
|
|
2214
|
+
}, [materializeConversation, pushMessage, setTextPrompt]);
|
|
2060
2215
|
const submitTextPrompt = useCallback((value) => {
|
|
2061
2216
|
const prompt = textPromptRef.current;
|
|
2062
2217
|
if (!prompt)
|
|
@@ -2124,8 +2279,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2124
2279
|
conversationMutationRef.current = 'fork';
|
|
2125
2280
|
setConversationMutation('fork');
|
|
2126
2281
|
try {
|
|
2127
|
-
const
|
|
2128
|
-
const
|
|
2282
|
+
const scope = await materializeConversation();
|
|
2283
|
+
const sessions = sessionsRef.current;
|
|
2129
2284
|
if (!sessions || !scope) {
|
|
2130
2285
|
pushMessage('system', 'Conversation history is unavailable in this folder.');
|
|
2131
2286
|
return;
|
|
@@ -2139,6 +2294,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2139
2294
|
// The transcript on screen is already the fork's history, so nothing
|
|
2140
2295
|
// is reloaded or reset. Only where the NEXT turn is written changes.
|
|
2141
2296
|
scope.sessionId = forked.id;
|
|
2297
|
+
resetSubagentActivity();
|
|
2142
2298
|
goalActivation.clear();
|
|
2143
2299
|
wakeGoalDriver();
|
|
2144
2300
|
pushMessage('system', `Forked into "${forked.title}" — ${forked.copied} message(s) copied. This screen continues in the copy; ${original} is unchanged and still in /resume.`);
|
|
@@ -2150,7 +2306,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2150
2306
|
conversationMutationRef.current = null;
|
|
2151
2307
|
setConversationMutation(null);
|
|
2152
2308
|
}
|
|
2153
|
-
}, [
|
|
2309
|
+
}, [
|
|
2310
|
+
goalActivation,
|
|
2311
|
+
hasUnsettledTurn,
|
|
2312
|
+
materializeConversation,
|
|
2313
|
+
pushMessage,
|
|
2314
|
+
resetSubagentActivity,
|
|
2315
|
+
wakeGoalDriver,
|
|
2316
|
+
]);
|
|
2154
2317
|
/** Open the durable prompt picker after the composer's second empty Esc. */
|
|
2155
2318
|
const openPromptEditor = useCallback(() => {
|
|
2156
2319
|
if (conversationMutationRef.current || compactingRef.current)
|
|
@@ -2206,6 +2369,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2206
2369
|
.slice(0, target.userOrdinal)
|
|
2207
2370
|
.map((prompt) => prompt.displayText);
|
|
2208
2371
|
conversationGenRef.current += 1;
|
|
2372
|
+
resetSubagentActivity();
|
|
2209
2373
|
activeTurnTokenRef.current = null;
|
|
2210
2374
|
goalActivation.clear();
|
|
2211
2375
|
wakeGoalDriver();
|
|
@@ -2246,6 +2410,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2246
2410
|
hasUnsettledTurn,
|
|
2247
2411
|
nextId,
|
|
2248
2412
|
pushMessage,
|
|
2413
|
+
resetSubagentActivity,
|
|
2249
2414
|
resetTranscript,
|
|
2250
2415
|
wakeGoalDriver,
|
|
2251
2416
|
]);
|
|
@@ -2259,15 +2424,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2259
2424
|
feedback: permissionReviewRefusal(review.reason),
|
|
2260
2425
|
});
|
|
2261
2426
|
}
|
|
2427
|
+
const summary = buildPermissionSummary(review.text);
|
|
2262
2428
|
return new Promise((resolve) => {
|
|
2263
2429
|
permissionResolveRef.current = resolve;
|
|
2264
2430
|
permissionOpenedAtRef.current = Date.now();
|
|
2265
2431
|
setPermissionReviewOffset(0);
|
|
2266
|
-
|
|
2432
|
+
setPermissionDetailsOpen(!summary.complete);
|
|
2433
|
+
setPermission({ ...req, review: review.text, summary });
|
|
2267
2434
|
setState('awaiting-permission');
|
|
2268
2435
|
sendTerminalNotification({ kind: 'approval-required' });
|
|
2269
2436
|
});
|
|
2270
|
-
}, [sendTerminalNotification, setPermissionReviewOffset]);
|
|
2437
|
+
}, [sendTerminalNotification, setPermissionDetailsOpen, setPermissionReviewOffset]);
|
|
2271
2438
|
// Render one agent event onto the transcript. Shared by the local turn loop
|
|
2272
2439
|
// and the daemon-attach poller, so both paths produce identical output.
|
|
2273
2440
|
// `st` carries the streaming-assistant bubble id + accumulated text across
|
|
@@ -2291,10 +2458,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2291
2458
|
switch (event.kind) {
|
|
2292
2459
|
case 'delta': {
|
|
2293
2460
|
setState('thinking');
|
|
2294
|
-
if (event.messageId && event.runId) {
|
|
2461
|
+
if (event.messageId && event.runId && st.sessionId) {
|
|
2295
2462
|
lastAssistantMessage.current = {
|
|
2296
2463
|
runId: event.runId,
|
|
2297
2464
|
messageId: event.messageId,
|
|
2465
|
+
sessionId: st.sessionId,
|
|
2298
2466
|
};
|
|
2299
2467
|
}
|
|
2300
2468
|
st.text += event.text;
|
|
@@ -2368,20 +2536,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2368
2536
|
}
|
|
2369
2537
|
case 'usage':
|
|
2370
2538
|
setUsage({ totalTokens: event.totalTokens, cost: event.cost });
|
|
2371
|
-
// Mirrored verbatim, absences included. A run that reports no
|
|
2372
|
-
// window clears the gauge rather than leaving the last one up:
|
|
2373
|
-
// a stale bar is read as current, and this bar's whole defect
|
|
2374
|
-
// was being read as something it was not.
|
|
2375
|
-
setContext({
|
|
2376
|
-
...(event.contextTokens !== undefined ? { tokens: event.contextTokens } : {}),
|
|
2377
|
-
...(event.contextWindowTokens !== undefined
|
|
2378
|
-
? { windowTokens: event.contextWindowTokens }
|
|
2379
|
-
: {}),
|
|
2380
|
-
...(event.contextMeasuredBy !== undefined
|
|
2381
|
-
? { measuredBy: event.contextMeasuredBy }
|
|
2382
|
-
: {}),
|
|
2383
|
-
...(event.windowSource !== undefined ? { windowSource: event.windowSource } : {}),
|
|
2384
|
-
});
|
|
2385
2539
|
break;
|
|
2386
2540
|
case 'task':
|
|
2387
2541
|
pushMessage('tool', event.subject, false, event.status === 'completed' ? '☑' : '☐');
|
|
@@ -2464,13 +2618,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2464
2618
|
// state idle while the admission read was pending let a second submit
|
|
2465
2619
|
// start beside it and broke the queue's FIFO ownership.
|
|
2466
2620
|
setState('thinking');
|
|
2621
|
+
let durableScope;
|
|
2622
|
+
try {
|
|
2623
|
+
durableScope = await materializeConversation();
|
|
2624
|
+
}
|
|
2625
|
+
catch (error) {
|
|
2626
|
+
pushMessage('system', `Conversation persistence could not start: ${error instanceof Error ? error.message : String(error)}. This turn will continue in memory and cannot be resumed.`);
|
|
2627
|
+
}
|
|
2467
2628
|
// A conversation can be closed outside this App after it was opened or
|
|
2468
2629
|
// resumed. Re-check at the actual turn boundary, before expanding file
|
|
2469
2630
|
// mentions, recording run evidence, or creating a provider iterator.
|
|
2470
2631
|
// The persistence helpers repeat the same check at their own boundary;
|
|
2471
2632
|
// neither read is claimed to serialize a concurrent archive (that needs
|
|
2472
2633
|
// a store-level lease shared with ProjectManager).
|
|
2473
|
-
const destination =
|
|
2634
|
+
const destination = conversationMaterializedRef.current
|
|
2635
|
+
? (durableScope?.sessionId ?? scopeRef.current?.sessionId ?? null)
|
|
2636
|
+
: null;
|
|
2474
2637
|
const turnSessions = sessionsRef.current;
|
|
2475
2638
|
if (turnSessions && destination) {
|
|
2476
2639
|
try {
|
|
@@ -2522,6 +2685,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2522
2685
|
pending: '',
|
|
2523
2686
|
completed: false,
|
|
2524
2687
|
outcome: null,
|
|
2688
|
+
sessionId: destination,
|
|
2525
2689
|
notification: null,
|
|
2526
2690
|
};
|
|
2527
2691
|
const ac = new AbortController();
|
|
@@ -2848,6 +3012,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2848
3012
|
finalizeMessage,
|
|
2849
3013
|
flushStream,
|
|
2850
3014
|
goalActivation,
|
|
3015
|
+
materializeConversation,
|
|
2851
3016
|
onPermission,
|
|
2852
3017
|
pushMessage,
|
|
2853
3018
|
replaceQueued,
|
|
@@ -2872,7 +3037,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2872
3037
|
? 'branched for prompt editing'
|
|
2873
3038
|
: conversationMutationRef.current === 'archive'
|
|
2874
3039
|
? 'archived'
|
|
2875
|
-
:
|
|
3040
|
+
: conversationMutationRef.current === 'materialize'
|
|
3041
|
+
? 'initialized'
|
|
3042
|
+
: 'moved to a fresh conversation';
|
|
2876
3043
|
pushMessage('system', `Conversation history is being ${operation}. Wait for it to finish before sending another command or prompt.`);
|
|
2877
3044
|
return;
|
|
2878
3045
|
}
|
|
@@ -2909,6 +3076,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2909
3076
|
});
|
|
2910
3077
|
return;
|
|
2911
3078
|
}
|
|
3079
|
+
case 'agent-cockpit': {
|
|
3080
|
+
const agents = subagentsRef.current;
|
|
3081
|
+
if (agents.length === 0) {
|
|
3082
|
+
pushMessage('system', 'No delegated agents are available in this conversation yet. When an Agent tool starts one, /agent opens its live activity and retained transcript.');
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const firstPhase = agentPhases(agents)[0];
|
|
3086
|
+
const firstAgent = firstPhase?.agents[0];
|
|
3087
|
+
if (!firstPhase || !firstAgent)
|
|
3088
|
+
return;
|
|
3089
|
+
setAgentSurface({
|
|
3090
|
+
kind: 'cockpit',
|
|
3091
|
+
selectedPhaseId: firstPhase.id,
|
|
3092
|
+
selectedId: firstAgent.viewId,
|
|
3093
|
+
focus: 'agents',
|
|
3094
|
+
});
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
2912
3097
|
case 'clear-screen':
|
|
2913
3098
|
setMessages([]);
|
|
2914
3099
|
resetTranscript();
|
|
@@ -3243,29 +3428,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3243
3428
|
// THIS session can answer from. The descriptors used for
|
|
3244
3429
|
// the merge above carry no store — they are names — so the
|
|
3245
3430
|
// registry is rebuilt here with the live one.
|
|
3246
|
-
const durableSessions = sessionsRef.current;
|
|
3247
|
-
const runScope = scopeRef.current;
|
|
3248
3431
|
const generation = conversationGenRef.current;
|
|
3249
3432
|
const goalCommand = slash.name === 'goal';
|
|
3250
3433
|
if (goalCommand)
|
|
3251
3434
|
goalCommandInFlightRef.current = true;
|
|
3252
|
-
const registry = new HostCommandRegistry();
|
|
3253
|
-
registry.register(kernelHostCommands({
|
|
3254
|
-
allowedAgentIds: session?.agentIds ?? [],
|
|
3255
|
-
...(durableSessions && runScope
|
|
3256
|
-
? {
|
|
3257
|
-
goal: {
|
|
3258
|
-
store: durableSessions.goals,
|
|
3259
|
-
sessionId: runScope.sessionId,
|
|
3260
|
-
tenantId: durableSessions.tenantId,
|
|
3261
|
-
activation: goalActivation,
|
|
3262
|
-
},
|
|
3263
|
-
}
|
|
3264
|
-
: {}),
|
|
3265
|
-
}));
|
|
3266
3435
|
void (async () => {
|
|
3436
|
+
let goalCommandReleased = false;
|
|
3437
|
+
const releaseGoalCommand = () => {
|
|
3438
|
+
if (!goalCommand || goalCommandReleased)
|
|
3439
|
+
return;
|
|
3440
|
+
goalCommandReleased = true;
|
|
3441
|
+
goalCommandInFlightRef.current = false;
|
|
3442
|
+
wakeGoalDriver();
|
|
3443
|
+
};
|
|
3267
3444
|
try {
|
|
3445
|
+
if (goalCommand)
|
|
3446
|
+
await materializeConversation();
|
|
3447
|
+
const durableSessions = sessionsRef.current;
|
|
3448
|
+
const runScope = scopeRef.current;
|
|
3449
|
+
const registry = new HostCommandRegistry();
|
|
3450
|
+
registry.register(kernelHostCommands({
|
|
3451
|
+
allowedAgentIds: session?.agentIds ?? [],
|
|
3452
|
+
...(durableSessions && runScope && conversationMaterializedRef.current
|
|
3453
|
+
? {
|
|
3454
|
+
goal: {
|
|
3455
|
+
store: durableSessions.goals,
|
|
3456
|
+
sessionId: runScope.sessionId,
|
|
3457
|
+
tenantId: durableSessions.tenantId,
|
|
3458
|
+
activation: goalActivation,
|
|
3459
|
+
},
|
|
3460
|
+
}
|
|
3461
|
+
: {}),
|
|
3462
|
+
}));
|
|
3268
3463
|
const outcome = await registry.dispatch(`/${slash.name} ${slash.args.join(' ')}`.trim());
|
|
3464
|
+
// The dispatch result is the durable boundary. Release the input
|
|
3465
|
+
// gate before publishing that result: once the operator can see a
|
|
3466
|
+
// completed `/goal` row, the next goal command must be admissible.
|
|
3467
|
+
// Keeping the gate until `finally` left one render-sized window in
|
|
3468
|
+
// which a visible success was followed by "still reaching durable
|
|
3469
|
+
// state" when the operator immediately typed `/goal resume`.
|
|
3470
|
+
releaseGoalCommand();
|
|
3269
3471
|
// A late readout from the conversation just left must not be
|
|
3270
3472
|
// painted as state of the one now on screen.
|
|
3271
3473
|
if (conversationGenRef.current !== generation)
|
|
@@ -3280,10 +3482,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3280
3482
|
pushMessage('system', `Could not run /${slash.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3281
3483
|
}
|
|
3282
3484
|
finally {
|
|
3283
|
-
|
|
3284
|
-
goalCommandInFlightRef.current = false;
|
|
3285
|
-
wakeGoalDriver();
|
|
3286
|
-
}
|
|
3485
|
+
releaseGoalCommand();
|
|
3287
3486
|
}
|
|
3288
3487
|
})();
|
|
3289
3488
|
return;
|
|
@@ -3301,6 +3500,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3301
3500
|
title: 'Rate the latest answer',
|
|
3302
3501
|
runId: target.runId,
|
|
3303
3502
|
messageId: target.messageId,
|
|
3503
|
+
sessionId: target.sessionId,
|
|
3304
3504
|
values,
|
|
3305
3505
|
options: [
|
|
3306
3506
|
{
|
|
@@ -3318,7 +3518,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3318
3518
|
pushMessage('system', 'Nothing to rate yet.');
|
|
3319
3519
|
return;
|
|
3320
3520
|
}
|
|
3321
|
-
recordFeedback(target.runId, slash.messageId, slash.rating, slash.note);
|
|
3521
|
+
recordFeedback(target.sessionId, target.runId, slash.messageId, slash.rating, slash.note);
|
|
3322
3522
|
return;
|
|
3323
3523
|
}
|
|
3324
3524
|
case 'review': {
|
|
@@ -3417,11 +3617,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3417
3617
|
await replaceConversation(sessions, destination, result.messages);
|
|
3418
3618
|
}
|
|
3419
3619
|
modelHistoryRef.current = result.messages;
|
|
3420
|
-
// The old gauge measured the history that was just replaced.
|
|
3421
|
-
// Clear it only after the durable projection and live model
|
|
3422
|
-
// history agree; until then the old measurement is still the
|
|
3423
|
-
// truthful one. The next run publishes a fresh measurement.
|
|
3424
|
-
setContext(null);
|
|
3425
3620
|
// How many user/assistant turns survived the pass.
|
|
3426
3621
|
// `keepRecentRows` explains why the transcript is trimmed
|
|
3427
3622
|
// to match rather than rebuilt from `result.messages`.
|
|
@@ -3906,13 +4101,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3906
4101
|
return;
|
|
3907
4102
|
}
|
|
3908
4103
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
3909
|
-
setSelectedEdit((index) => moveSelection(index, editList.length, key.home
|
|
3910
|
-
? 'first'
|
|
3911
|
-
: key.end
|
|
3912
|
-
? 'last'
|
|
3913
|
-
: key.pageUp
|
|
3914
|
-
? 'previous-page'
|
|
3915
|
-
: 'next-page'));
|
|
4104
|
+
setSelectedEdit((index) => moveSelection(index, editList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
3916
4105
|
return;
|
|
3917
4106
|
}
|
|
3918
4107
|
if (key.escape || key.leftArrow || key.upArrow) {
|
|
@@ -3939,13 +4128,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3939
4128
|
if (resumeCommittedRef.current)
|
|
3940
4129
|
return;
|
|
3941
4130
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
3942
|
-
setSelectedResume((index) => moveSelection(index, resumeList.length, key.home
|
|
3943
|
-
? 'first'
|
|
3944
|
-
: key.end
|
|
3945
|
-
? 'last'
|
|
3946
|
-
: key.pageUp
|
|
3947
|
-
? 'previous-page'
|
|
3948
|
-
: 'next-page'));
|
|
4131
|
+
setSelectedResume((index) => moveSelection(index, resumeList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
3949
4132
|
}
|
|
3950
4133
|
else if (key.upArrow)
|
|
3951
4134
|
setSelectedResume((index) => moveSelection(index, resumeList.length, 'previous'));
|
|
@@ -3989,9 +4172,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3989
4172
|
}
|
|
3990
4173
|
// A pending permission prompt owns the keyboard: y/n/a decide it.
|
|
3991
4174
|
if (permissionResolveRef.current) {
|
|
4175
|
+
const ch = input.toLowerCase();
|
|
4176
|
+
if (permission && ch === 'd') {
|
|
4177
|
+
setPermissionReviewOffset(0);
|
|
4178
|
+
setPermissionDetailsOpen(!permissionDetailsOpenRef.current);
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
3992
4181
|
if (permission &&
|
|
3993
4182
|
(key.upArrow || key.downArrow || key.pageUp || key.pageDown || key.home || key.end)) {
|
|
3994
|
-
const
|
|
4183
|
+
const source = permissionDetailsOpenRef.current
|
|
4184
|
+
? permission.review
|
|
4185
|
+
: permission.summary.text;
|
|
4186
|
+
const count = permissionReviewRows(source, terminal.columns).length;
|
|
3995
4187
|
const maxOffset = Math.max(0, count - PERMISSION_REVIEW_PAGE_ROWS);
|
|
3996
4188
|
const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
|
|
3997
4189
|
const next = key.home
|
|
@@ -4008,7 +4200,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4008
4200
|
setPermissionReviewOffset(next);
|
|
4009
4201
|
return;
|
|
4010
4202
|
}
|
|
4011
|
-
const ch = input.toLowerCase();
|
|
4012
4203
|
// Refusing is never deferred or gated — it is the direction a
|
|
4013
4204
|
// mistake is recoverable in, so it answers on the first press.
|
|
4014
4205
|
if (key.ctrl && input === 'c') {
|
|
@@ -4034,6 +4225,125 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4034
4225
|
resolvePermission({ kind: 'approve-all' });
|
|
4035
4226
|
return;
|
|
4036
4227
|
}
|
|
4228
|
+
// Child observation owns navigation, but never outranks consent: the
|
|
4229
|
+
// permission branch above preempts it if a child or parent call asks.
|
|
4230
|
+
const agentView = agentSurfaceRef.current;
|
|
4231
|
+
if (agentView) {
|
|
4232
|
+
// The Return that opened `/agent` cannot also drill into the
|
|
4233
|
+
// highlighted child before the list has appeared on screen.
|
|
4234
|
+
if (agentSurfaceCommittedRef.current !== agentView)
|
|
4235
|
+
return;
|
|
4236
|
+
const agents = subagentsRef.current;
|
|
4237
|
+
if (agentView.kind === 'cockpit') {
|
|
4238
|
+
const phases = agentPhases(agents);
|
|
4239
|
+
const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
|
|
4240
|
+
const phase = phases[phaseIndex];
|
|
4241
|
+
const phaseAgents = phase?.agents ?? [];
|
|
4242
|
+
const agentIndex = Math.max(0, phaseAgents.findIndex((agent) => agent.viewId === agentView.selectedId));
|
|
4243
|
+
const selected = phaseAgents[agentIndex];
|
|
4244
|
+
if (!phase || !selected) {
|
|
4245
|
+
setAgentSurface(null);
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4249
|
+
setAgentSurface(null);
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
4253
|
+
const focus = key.leftArrow
|
|
4254
|
+
? 'phases'
|
|
4255
|
+
: key.rightArrow
|
|
4256
|
+
? 'agents'
|
|
4257
|
+
: agentView.focus === 'phases'
|
|
4258
|
+
? 'agents'
|
|
4259
|
+
: 'phases';
|
|
4260
|
+
setAgentSurface({ ...agentView, focus });
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
if (key.return) {
|
|
4264
|
+
if (agentView.focus === 'phases') {
|
|
4265
|
+
setAgentSurface({ ...agentView, focus: 'agents' });
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
setAgentSurface({
|
|
4269
|
+
kind: 'transcript',
|
|
4270
|
+
selectedPhaseId: phase.id,
|
|
4271
|
+
selectedId: selected.viewId,
|
|
4272
|
+
returnFocus: agentView.focus,
|
|
4273
|
+
tailOffset: 0,
|
|
4274
|
+
});
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
|
|
4278
|
+
const movement = key.home
|
|
4279
|
+
? 'first'
|
|
4280
|
+
: key.end
|
|
4281
|
+
? 'last'
|
|
4282
|
+
: key.pageUp
|
|
4283
|
+
? 'previous-page'
|
|
4284
|
+
: key.pageDown
|
|
4285
|
+
? 'next-page'
|
|
4286
|
+
: key.upArrow
|
|
4287
|
+
? 'previous'
|
|
4288
|
+
: 'next';
|
|
4289
|
+
if (agentView.focus === 'phases') {
|
|
4290
|
+
const next = moveSelection(phaseIndex, phases.length, movement, agentPhasePageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
|
|
4291
|
+
const targetPhase = phases[next];
|
|
4292
|
+
const targetAgent = targetPhase?.agents[0];
|
|
4293
|
+
if (targetPhase && targetAgent) {
|
|
4294
|
+
setAgentSurface({
|
|
4295
|
+
...agentView,
|
|
4296
|
+
selectedPhaseId: targetPhase.id,
|
|
4297
|
+
selectedId: targetAgent.viewId,
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
else {
|
|
4302
|
+
const next = moveSelection(agentIndex, phaseAgents.length, movement, agentPickerPageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
|
|
4303
|
+
const target = phaseAgents[next];
|
|
4304
|
+
if (target)
|
|
4305
|
+
setAgentSurface({ ...agentView, selectedId: target.viewId });
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
const selected = agents.find((agent) => agent.viewId === agentView.selectedId);
|
|
4311
|
+
if (!selected) {
|
|
4312
|
+
setAgentSurface(null);
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
if (input.toLowerCase() === 'q' || (key.ctrl && input === 'c')) {
|
|
4316
|
+
setAgentSurface(null);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
if (key.escape) {
|
|
4320
|
+
setAgentSurface({
|
|
4321
|
+
kind: 'cockpit',
|
|
4322
|
+
selectedPhaseId: agentView.selectedPhaseId,
|
|
4323
|
+
selectedId: selected.viewId,
|
|
4324
|
+
focus: agentView.returnFocus,
|
|
4325
|
+
});
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
|
|
4329
|
+
const page = agentTranscriptPageSize(terminal.rows);
|
|
4330
|
+
const max = maxAgentTranscriptTailOffset(selected, terminal.rows, terminal.columns);
|
|
4331
|
+
const offset = Math.min(agentView.tailOffset, max);
|
|
4332
|
+
const next = key.home
|
|
4333
|
+
? max
|
|
4334
|
+
: key.end
|
|
4335
|
+
? 0
|
|
4336
|
+
: key.pageUp
|
|
4337
|
+
? Math.min(max, offset + page)
|
|
4338
|
+
: key.pageDown
|
|
4339
|
+
? Math.max(0, offset - page)
|
|
4340
|
+
: key.upArrow
|
|
4341
|
+
? Math.min(max, offset + 1)
|
|
4342
|
+
: Math.max(0, offset - 1);
|
|
4343
|
+
setAgentSurface({ ...agentView, tailOffset: next });
|
|
4344
|
+
}
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4037
4347
|
// A host-owned text prompt has its own input hook. This synchronous ref
|
|
4038
4348
|
// claims the interval before React paints it too, so App cannot read a
|
|
4039
4349
|
// key repeat as an interrupt, exit ladder, or finite-menu choice.
|
|
@@ -4058,13 +4368,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4058
4368
|
}
|
|
4059
4369
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
4060
4370
|
const pageSize = picker.kind === 'command' ? picker.windowSize : undefined;
|
|
4061
|
-
setSelectedChoice((index) => moveSelection(index, options.length, key.home
|
|
4062
|
-
? 'first'
|
|
4063
|
-
: key.end
|
|
4064
|
-
? 'last'
|
|
4065
|
-
: key.pageUp
|
|
4066
|
-
? 'previous-page'
|
|
4067
|
-
: 'next-page', pageSize));
|
|
4371
|
+
setSelectedChoice((index) => moveSelection(index, options.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page', pageSize));
|
|
4068
4372
|
return;
|
|
4069
4373
|
}
|
|
4070
4374
|
if (key.upArrow) {
|
|
@@ -4090,19 +4394,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4090
4394
|
// stays above it in this handler so an approval request that arrives
|
|
4091
4395
|
// while the picker is open cannot have its keys stolen by the chooser.
|
|
4092
4396
|
if (copyPickerRef.current) {
|
|
4397
|
+
// The Return that submitted `/copy` is an ownership fence, not a
|
|
4398
|
+
// selection made against a picker the operator has not seen yet.
|
|
4399
|
+
if (copyPickerCommittedRef.current !== copyPickerRef.current)
|
|
4400
|
+
return;
|
|
4093
4401
|
const targets = copyPickerRef.current.targets;
|
|
4094
4402
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
4095
4403
|
setCopyPicker(null);
|
|
4096
4404
|
return;
|
|
4097
4405
|
}
|
|
4098
4406
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
4099
|
-
setSelectedCopy((index) => moveSelection(index, targets.length, key.home
|
|
4100
|
-
? 'first'
|
|
4101
|
-
: key.end
|
|
4102
|
-
? 'last'
|
|
4103
|
-
: key.pageUp
|
|
4104
|
-
? 'previous-page'
|
|
4105
|
-
: 'next-page'));
|
|
4407
|
+
setSelectedCopy((index) => moveSelection(index, targets.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
4106
4408
|
return;
|
|
4107
4409
|
}
|
|
4108
4410
|
if (key.upArrow) {
|
|
@@ -4213,21 +4515,73 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4213
4515
|
// Background is left natural — we inherit the terminal's own background
|
|
4214
4516
|
// and only theme the foreground. Forcing
|
|
4215
4517
|
// a filled bg left mismatched patches around bordered areas, so we don't.
|
|
4216
|
-
const
|
|
4217
|
-
|
|
4518
|
+
const liveSubagentCount = subagents.filter((agent) => agent.status === 'starting' || agent.status === 'working').length;
|
|
4519
|
+
const selectedSubagent = agentSurface
|
|
4520
|
+
? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
|
|
4521
|
+
: undefined;
|
|
4522
|
+
const lifecycleOwnsViewport = phase === 'trust' ||
|
|
4523
|
+
phase === 'resume' ||
|
|
4524
|
+
phase === 'edit' ||
|
|
4525
|
+
phase === 'picker' ||
|
|
4526
|
+
agentSurface !== null;
|
|
4527
|
+
const statusGoal = phase === 'ready' &&
|
|
4528
|
+
state === 'idle' &&
|
|
4529
|
+
conversationMutation === null &&
|
|
4530
|
+
!compacting &&
|
|
4531
|
+
permission === null &&
|
|
4532
|
+
textPrompt === null &&
|
|
4533
|
+
choicePicker === null &&
|
|
4534
|
+
copyPicker === null &&
|
|
4535
|
+
agentSurface === null
|
|
4536
|
+
? goalStatusLabel(goalStatus)
|
|
4537
|
+
: null;
|
|
4538
|
+
const statusHint = conversationMutation === 'fork'
|
|
4539
|
+
? 'forking conversation — input is paused'
|
|
4540
|
+
: conversationMutation === 'edit'
|
|
4541
|
+
? 'branching before prompt — input is paused'
|
|
4542
|
+
: conversationMutation === 'archive'
|
|
4543
|
+
? 'archiving conversation — input is paused'
|
|
4544
|
+
: conversationMutation === 'new'
|
|
4545
|
+
? 'starting a fresh conversation — input is paused'
|
|
4546
|
+
: compacting
|
|
4547
|
+
? 'compacting conversation — input is paused'
|
|
4548
|
+
: permission
|
|
4549
|
+
? hintForPhase(phase, state, session?.hasProvider === true)
|
|
4550
|
+
: agentSurface?.kind === 'cockpit'
|
|
4551
|
+
? agentSurface.focus === 'phases'
|
|
4552
|
+
? 'agent phases — enter agents · esc return'
|
|
4553
|
+
: 'agents — enter inspect · left phases · esc return'
|
|
4554
|
+
: agentSurface?.kind === 'transcript'
|
|
4555
|
+
? 'observing agent — esc agents · q parent'
|
|
4556
|
+
: textPrompt
|
|
4557
|
+
? 'name editor open — enter save · esc cancel'
|
|
4558
|
+
: choicePicker
|
|
4559
|
+
? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
|
|
4560
|
+
: copyPicker
|
|
4561
|
+
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
4562
|
+
: goalStatus && phase === 'ready' && state === 'idle'
|
|
4563
|
+
? undefined
|
|
4564
|
+
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
4565
|
+
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: activeTools, working: state === 'thinking' || state === 'tool', agentCount: liveSubagentCount, interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
|
|
4218
4566
|
phase === 'ready' &&
|
|
4219
4567
|
conversationMutation === null &&
|
|
4220
4568
|
textPrompt === null &&
|
|
4221
4569
|
choicePicker === null &&
|
|
4222
|
-
copyPicker === null
|
|
4570
|
+
copyPicker === null &&
|
|
4571
|
+
agentSurface === null, hidden: permission !== null ||
|
|
4572
|
+
choicePicker !== null ||
|
|
4573
|
+
copyPicker !== null ||
|
|
4574
|
+
agentSurface !== null, children: [pendingSteers.length > 0 &&
|
|
4223
4575
|
permission === null &&
|
|
4224
4576
|
textPrompt === null &&
|
|
4225
4577
|
choicePicker === null &&
|
|
4226
|
-
copyPicker === null
|
|
4578
|
+
copyPicker === null &&
|
|
4579
|
+
agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
|
|
4227
4580
|
permission === null &&
|
|
4228
4581
|
textPrompt === null &&
|
|
4229
4582
|
choicePicker === null &&
|
|
4230
|
-
copyPicker === null
|
|
4583
|
+
copyPicker === null &&
|
|
4584
|
+
agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
4231
4585
|
? queuePause.outcome === 'paused'
|
|
4232
4586
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
4233
4587
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
@@ -4235,42 +4589,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4235
4589
|
state === 'awaiting-permission' ||
|
|
4236
4590
|
compacting ||
|
|
4237
4591
|
conversationMutation !== null ||
|
|
4238
|
-
externalEditorRequest !== null
|
|
4592
|
+
externalEditorRequest !== null ||
|
|
4593
|
+
agentSurface !== null, hidden: permission !== null ||
|
|
4239
4594
|
textPrompt !== null ||
|
|
4240
4595
|
choicePicker !== null ||
|
|
4241
|
-
copyPicker !== null
|
|
4596
|
+
copyPicker !== null ||
|
|
4597
|
+
agentSurface !== null,
|
|
4242
4598
|
// A turn is running, so Esc is the interrupt and not
|
|
4243
4599
|
// the composer's clear.
|
|
4244
|
-
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), userCommands: userCommands, mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal:
|
|
4245
|
-
state === 'idle' &&
|
|
4246
|
-
conversationMutation === null &&
|
|
4247
|
-
!compacting &&
|
|
4248
|
-
permission === null &&
|
|
4249
|
-
textPrompt === null &&
|
|
4250
|
-
choicePicker === null &&
|
|
4251
|
-
copyPicker === null
|
|
4252
|
-
? goalStatusLabel(goalStatus)
|
|
4253
|
-
: null, state: state, hint: conversationMutation === 'fork'
|
|
4254
|
-
? 'forking conversation — input is paused'
|
|
4255
|
-
: conversationMutation === 'edit'
|
|
4256
|
-
? 'branching before prompt — input is paused'
|
|
4257
|
-
: conversationMutation === 'archive'
|
|
4258
|
-
? 'archiving conversation — input is paused'
|
|
4259
|
-
: conversationMutation === 'new'
|
|
4260
|
-
? 'starting a fresh conversation — input is paused'
|
|
4261
|
-
: compacting
|
|
4262
|
-
? 'compacting conversation — input is paused'
|
|
4263
|
-
: permission
|
|
4264
|
-
? hintForPhase(phase, state, session?.hasProvider === true)
|
|
4265
|
-
: textPrompt
|
|
4266
|
-
? 'name editor open — enter save · esc cancel'
|
|
4267
|
-
: choicePicker
|
|
4268
|
-
? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
|
|
4269
|
-
: copyPicker
|
|
4270
|
-
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
4271
|
-
: goalStatus && phase === 'ready' && state === 'idle'
|
|
4272
|
-
? undefined
|
|
4273
|
-
: hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] }) }));
|
|
4600
|
+
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), 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, userCommands: userCommands, mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
|
|
4274
4601
|
}
|
|
4275
4602
|
function Banner({ version, session, permissionMode, cwd, }) {
|
|
4276
4603
|
const { columns } = useWindowSize();
|
|
@@ -4302,7 +4629,9 @@ function formatToolCall(toolName, summary, standalone = false) {
|
|
|
4302
4629
|
}
|
|
4303
4630
|
function hintForPhase(phase, state,
|
|
4304
4631
|
/** Whether there is a working session behind the picker to go back to. */
|
|
4305
|
-
canReturn = false
|
|
4632
|
+
canReturn = false,
|
|
4633
|
+
/** Whether the active-turn composer contains text, a paste, or an attachment. */
|
|
4634
|
+
hasDraft = false) {
|
|
4306
4635
|
// Enter is absent because Enter grants nothing here, deliberately — see the
|
|
4307
4636
|
// trust branch in the key handler above.
|
|
4308
4637
|
if (phase === 'trust')
|
|
@@ -4328,7 +4657,7 @@ canReturn = false) {
|
|
|
4328
4657
|
if (state === 'awaiting-permission')
|
|
4329
4658
|
return 'y approve · n / esc reject · a approve all';
|
|
4330
4659
|
if (state !== 'idle')
|
|
4331
|
-
return 'enter steer · tab queue · esc interrupt';
|
|
4660
|
+
return hasDraft ? 'enter steer · tab queue · esc interrupt' : '';
|
|
4332
4661
|
// The composer already points to /help. Keeping the complete key legend in
|
|
4333
4662
|
// every idle frame made ordinary conversation read like a permanent setup
|
|
4334
4663
|
// screen; only state-specific actions belong in the footer.
|