@namzu/cli 34.1.0 → 35.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +21 -7
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +179 -6
- package/dist/commands/mcp.js.map +1 -1
- package/dist/doctor/checks/credentials.d.ts.map +1 -1
- package/dist/doctor/checks/credentials.js +19 -4
- package/dist/doctor/checks/credentials.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +107 -15
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts +2 -0
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +8 -5
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/setup.d.ts.map +1 -1
- package/dist/integrations/providers/setup.js +3 -1
- package/dist/integrations/providers/setup.js.map +1 -1
- package/dist/integrations/resident/session-step.d.ts.map +1 -1
- package/dist/integrations/resident/session-step.js +13 -8
- package/dist/integrations/resident/session-step.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +385 -95
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BackgroundJobsPanel.d.ts +26 -0
- package/dist/tui/BackgroundJobsPanel.d.ts.map +1 -0
- package/dist/tui/BackgroundJobsPanel.js +129 -0
- package/dist/tui/BackgroundJobsPanel.js.map +1 -0
- package/dist/tui/Composer.d.ts +7 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +65 -34
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/PendingInputPanel.d.ts +34 -0
- package/dist/tui/PendingInputPanel.d.ts.map +1 -0
- package/dist/tui/PendingInputPanel.js +108 -0
- package/dist/tui/PendingInputPanel.js.map +1 -0
- package/dist/tui/agent.d.ts +10 -3
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +78 -12
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +5 -1
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +9 -8
- package/dist/tui/slashCommands.js.map +1 -1
- package/package.json +5 -5
package/dist/tui/App.js
CHANGED
|
@@ -16,6 +16,7 @@ import { TURN_LIMIT_FIELDS, turnLimitCommands } from './turn-limits-settings.js'
|
|
|
16
16
|
import { discoverProviders } from '../integrations/providers/discover.js';
|
|
17
17
|
import { ProviderSetup } from './ProviderSetup.js';
|
|
18
18
|
import { ToolOutputViewer } from './ToolOutputViewer.js';
|
|
19
|
+
import { BackgroundJobsPanel, backgroundJobOutputPageSize, backgroundJobsPageSize, maxBackgroundJobTailOffset, } from './BackgroundJobsPanel.js';
|
|
19
20
|
import { join, relative } from 'node:path';
|
|
20
21
|
import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, SKILL_TOOL_NAME, } from '@namzu/sdk';
|
|
21
22
|
import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
|
|
@@ -52,6 +53,7 @@ import { unavailable as triggerUnavailable } from './triggers/detect.js';
|
|
|
52
53
|
import { BUILTIN_TRIGGERS, compileRegistry, triggerDefinition, } from './triggers/registry.js';
|
|
53
54
|
import { describeComposerTriggers, setComposerTriggers } from './triggers/setting.js';
|
|
54
55
|
import { ComposerFrame } from './ComposerFrame.js';
|
|
56
|
+
import { PendingInputPanel, pendingInputPanelRows } from './PendingInputPanel.js';
|
|
55
57
|
import { EffortSlider, effortSliderLayout } from './EffortSlider.js';
|
|
56
58
|
import { HYPERMODE, HYPERMODE_SUMMARY, ORCHESTRATE_ALIAS_NOTICE, hypermodeEffort, hypermodeStopLabel, } from './hypermode.js';
|
|
57
59
|
import { CopyPicker } from './CopyPicker.js';
|
|
@@ -229,14 +231,20 @@ export function projectConversation(messages, nextId, readableUserTexts = []) {
|
|
|
229
231
|
return [];
|
|
230
232
|
});
|
|
231
233
|
}
|
|
234
|
+
function isRecallableHumanPrompt(prompt) {
|
|
235
|
+
return prompt?.kind === 'human' && prompt.recallable === true;
|
|
236
|
+
}
|
|
232
237
|
/** Put an undelivered steer back where it occurred among Tab-queued prompts. */
|
|
233
238
|
function mergeUndeliveredLiveInput(queued, undelivered) {
|
|
234
239
|
const merged = [...queued];
|
|
235
|
-
let
|
|
240
|
+
let lastInsertedIndex = -1;
|
|
236
241
|
for (const input of undelivered) {
|
|
237
|
-
|
|
242
|
+
let lastPredecessorIndex = -1;
|
|
243
|
+
for (const predecessor of input.queuedBefore)
|
|
244
|
+
lastPredecessorIndex = Math.max(lastPredecessorIndex, merged.indexOf(predecessor));
|
|
245
|
+
const index = Math.max(lastPredecessorIndex, lastInsertedIndex) + 1;
|
|
238
246
|
merged.splice(index, 0, input.prompt);
|
|
239
|
-
|
|
247
|
+
lastInsertedIndex = index;
|
|
240
248
|
}
|
|
241
249
|
return merged;
|
|
242
250
|
}
|
|
@@ -451,23 +459,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
451
459
|
summaries: 0,
|
|
452
460
|
reclaimedTokens: 0,
|
|
453
461
|
});
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
462
|
+
// Keep exits until the kernel acknowledges delivery or a later completed
|
|
463
|
+
// turn carries them. An exit after the last tool result still needs that
|
|
464
|
+
// next turn, while every exit gets one immediate transcript row.
|
|
465
|
+
const idleJobNoticesRef = useRef({ ownerSession: null, pending: new Map(), delivered: new Set(), shown: new Set() });
|
|
466
|
+
const [jobSnapshot, setJobSnapshot] = useState({ ownerSession: null, jobs: [] });
|
|
467
|
+
const backgroundJobs = jobSnapshot.ownerSession === session ? jobSnapshot.jobs : [];
|
|
468
|
+
const snapshotIdleJobNotices = () => {
|
|
469
|
+
if (idleJobNoticesRef.current.ownerSession !== session)
|
|
470
|
+
return { ids: [] };
|
|
471
|
+
const pending = idleJobNoticesRef.current.pending;
|
|
472
|
+
if (pending.size === 0)
|
|
473
|
+
return { ids: [] };
|
|
474
|
+
const lines = [...pending.values()];
|
|
475
|
+
return {
|
|
476
|
+
ids: [...pending.keys()],
|
|
477
|
+
text: `Background jobs that ended since your last turn:\n${lines.map((l) => `- ${l}`).join('\n')}`,
|
|
478
|
+
};
|
|
471
479
|
};
|
|
472
480
|
// Commands the operator ran with `!` since the last turn. The model did
|
|
473
481
|
// not see them happen; it reads them here on its next turn.
|
|
@@ -480,11 +488,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
480
488
|
return `What the operator did outside you since your last turn — commands they ran in their own shell, files they put back — with the output:\n\n${blocks.join('\n\n')}`;
|
|
481
489
|
};
|
|
482
490
|
useEffect(() => {
|
|
491
|
+
const notices = {
|
|
492
|
+
ownerSession: session,
|
|
493
|
+
pending: new Map(),
|
|
494
|
+
delivered: new Set(),
|
|
495
|
+
shown: new Set(),
|
|
496
|
+
};
|
|
497
|
+
idleJobNoticesRef.current = notices;
|
|
498
|
+
setJobSnapshot({ ownerSession: session, jobs: session?.jobs?.() ?? [] });
|
|
483
499
|
if (!session?.onJobExit)
|
|
484
500
|
return;
|
|
485
501
|
return session.onJobExit((job) => {
|
|
486
|
-
|
|
502
|
+
// The visible running-shell count belongs to the session even when
|
|
503
|
+
// the kernel owns this exit's model/transcript notice.
|
|
504
|
+
if (previousSessionRef.current !== session || idleJobNoticesRef.current !== notices)
|
|
487
505
|
return;
|
|
506
|
+
setJobSnapshot({ ownerSession: session, jobs: session.jobs?.() ?? [] });
|
|
488
507
|
const text = describeJobExit({
|
|
489
508
|
jobId: job.id,
|
|
490
509
|
command: job.command,
|
|
@@ -492,8 +511,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
492
511
|
...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
|
|
493
512
|
...(job.signal ? { signal: job.signal } : {}),
|
|
494
513
|
});
|
|
495
|
-
|
|
496
|
-
|
|
514
|
+
// SDK delivery can run before this listener on a host with different
|
|
515
|
+
// subscription order. The tombstone keeps that acknowledged exit out of
|
|
516
|
+
// the next turn even when its UI event arrives later.
|
|
517
|
+
if (!notices.delivered.has(job.id))
|
|
518
|
+
notices.pending.set(job.id, text);
|
|
519
|
+
if (!notices.shown.has(job.id)) {
|
|
520
|
+
notices.shown.add(job.id);
|
|
521
|
+
pushMessage('system', text, false, 'J');
|
|
522
|
+
}
|
|
497
523
|
});
|
|
498
524
|
}, [session]);
|
|
499
525
|
const [usage, setUsage] = useState(null);
|
|
@@ -541,6 +567,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
541
567
|
const settledRef = useRef(0);
|
|
542
568
|
// Complete prompts waiting for the one queue pump that may start a turn.
|
|
543
569
|
const [queued, setQueued] = useState([]);
|
|
570
|
+
// Esc can make the screen idle before an aborted iterator settles. Recheck
|
|
571
|
+
// the queue when that iterator releases its turn token, even if `idle` did
|
|
572
|
+
// not change and no new prompt was appended in its finally block.
|
|
573
|
+
const [queueSettleVersion, setQueueSettleVersion] = useState(0);
|
|
544
574
|
/** Return-submitted prompts accepted by the active turn but not yet drained by the SDK. */
|
|
545
575
|
const [pendingSteers, setPendingSteers] = useState([]);
|
|
546
576
|
// Kept synchronous with the rendered queue so a turn's `finally` can decide
|
|
@@ -673,6 +703,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
673
703
|
*/
|
|
674
704
|
const [narration, setNarration] = useState([]);
|
|
675
705
|
const [agentSurface, setAgentSurfaceState] = useState(null);
|
|
706
|
+
const [jobSurfaceState, setJobSurfaceState] = useState(null);
|
|
707
|
+
const jobSurface = jobSurfaceState?.ownerSession === session ? jobSurfaceState : null;
|
|
708
|
+
const jobSurfaceRef = useRef(null);
|
|
709
|
+
const jobSurfaceCommittedRef = useRef(null);
|
|
710
|
+
const setJobSurface = useCallback((next) => {
|
|
711
|
+
const wasVisible = jobSurfaceCommittedRef.current !== null;
|
|
712
|
+
jobSurfaceRef.current = next;
|
|
713
|
+
if (next === null)
|
|
714
|
+
jobSurfaceCommittedRef.current = null;
|
|
715
|
+
else if (wasVisible)
|
|
716
|
+
jobSurfaceCommittedRef.current = next;
|
|
717
|
+
setJobSurfaceState(next);
|
|
718
|
+
}, []);
|
|
719
|
+
useLayoutEffect(() => {
|
|
720
|
+
jobSurfaceCommittedRef.current = jobSurface;
|
|
721
|
+
}, [jobSurface]);
|
|
722
|
+
useEffect(() => setJobSurface(null), [session, setJobSurface]);
|
|
723
|
+
const [jobClock, setJobClock] = useState(Date.now());
|
|
724
|
+
const hasRunningBackgroundJob = backgroundJobs.some((job) => job.status === 'running');
|
|
725
|
+
const jobSurfaceOpen = jobSurface !== null;
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
if (!jobSurfaceOpen || !hasRunningBackgroundJob)
|
|
728
|
+
return;
|
|
729
|
+
const timer = setInterval(() => {
|
|
730
|
+
setJobClock(Date.now());
|
|
731
|
+
setJobSnapshot({ ownerSession: session, jobs: session?.jobs?.() ?? [] });
|
|
732
|
+
}, 1000);
|
|
733
|
+
return () => clearInterval(timer);
|
|
734
|
+
}, [jobSurfaceOpen, hasRunningBackgroundJob, session]);
|
|
676
735
|
const agentSurfaceRef = useRef(null);
|
|
677
736
|
const agentSurfaceCommittedRef = useRef(null);
|
|
678
737
|
const setAgentSurface = useCallback((next) => {
|
|
@@ -1009,16 +1068,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1009
1068
|
const [saveSkillPrompt, setSaveSkillPromptState] = useState(null);
|
|
1010
1069
|
const saveSkillPromptRef = useRef(null);
|
|
1011
1070
|
const setSaveSkillPrompt = useCallback((next) => {
|
|
1071
|
+
if (next)
|
|
1072
|
+
setJobSurface(null);
|
|
1012
1073
|
saveSkillPromptRef.current = next;
|
|
1013
1074
|
setSaveSkillPromptState(next);
|
|
1014
|
-
}, []);
|
|
1075
|
+
}, [setJobSurface]);
|
|
1015
1076
|
const [scheduleReviewPrompt, setScheduleReviewPromptState] = useState(null);
|
|
1016
1077
|
const scheduleReviewTokenRef = useRef(0);
|
|
1017
1078
|
const scheduleReviewPromptRef = useRef(null);
|
|
1018
1079
|
const setScheduleReviewPrompt = useCallback((next) => {
|
|
1080
|
+
if (next)
|
|
1081
|
+
setJobSurface(null);
|
|
1019
1082
|
scheduleReviewPromptRef.current = next;
|
|
1020
1083
|
setScheduleReviewPromptState(next);
|
|
1021
|
-
}, []);
|
|
1084
|
+
}, [setJobSurface]);
|
|
1022
1085
|
const saveSkillNotifyRef = useRef(null);
|
|
1023
1086
|
const saveSkillToolRef = useRef(null);
|
|
1024
1087
|
if (saveSkillToolRef.current === null) {
|
|
@@ -1314,6 +1377,28 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1314
1377
|
]);
|
|
1315
1378
|
return id;
|
|
1316
1379
|
}, [nextId]);
|
|
1380
|
+
const recallQueuedPrompt = useCallback(() => {
|
|
1381
|
+
const pending = queuedRef.current;
|
|
1382
|
+
let index = pending.length - 1;
|
|
1383
|
+
while (index >= 0 && !isRecallableHumanPrompt(pending[index]))
|
|
1384
|
+
index -= 1;
|
|
1385
|
+
if (index < 0) {
|
|
1386
|
+
pushMessage('system', 'No queued message to edit.');
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
const prompt = pending[index];
|
|
1390
|
+
if (!isRecallableHumanPrompt(prompt))
|
|
1391
|
+
return null;
|
|
1392
|
+
const remaining = [...pending.slice(0, index), ...pending.slice(index + 1)];
|
|
1393
|
+
replaceQueued(remaining);
|
|
1394
|
+
if (remaining.length === 0)
|
|
1395
|
+
setQueuePause(null);
|
|
1396
|
+
return {
|
|
1397
|
+
text: prompt.recallText ?? prompt.text,
|
|
1398
|
+
...(prompt.attachments ? { attachments: prompt.attachments } : {}),
|
|
1399
|
+
...(prompt.triggers ? { triggers: prompt.triggers } : {}),
|
|
1400
|
+
};
|
|
1401
|
+
}, [pushMessage, replaceQueued, setQueuePause]);
|
|
1317
1402
|
/**
|
|
1318
1403
|
* The skill proposal's per-conversation facts: whether this conversation
|
|
1319
1404
|
* has proposed one, and whether it has used a skill. Keyed by the
|
|
@@ -3056,6 +3141,30 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3056
3141
|
}
|
|
3057
3142
|
}, [activateTrustedProject, pushMessage, runProbe]);
|
|
3058
3143
|
const finalized = messages.filter((m) => !m.pending);
|
|
3144
|
+
const runningShellCount = backgroundJobs.filter((job) => job.status === 'running').length;
|
|
3145
|
+
const shellFurniture = runningShellCount > 0 ? 1 : 0;
|
|
3146
|
+
const pendingInputItems = pendingSteers.map((input) => ({
|
|
3147
|
+
text: input.prompt.recallText ?? input.prompt.text,
|
|
3148
|
+
attachmentCount: (input.prompt.attachments?.length ?? 0) + input.attachedFiles,
|
|
3149
|
+
}));
|
|
3150
|
+
const queuedInputItems = queued.map((prompt) => ({
|
|
3151
|
+
text: prompt.kind === 'goal' ? 'Automatic goal continuation' : prompt.recallText ?? prompt.text,
|
|
3152
|
+
attachmentCount: prompt.kind === 'human' ? prompt.attachments?.length : undefined,
|
|
3153
|
+
}));
|
|
3154
|
+
const recallableQueued = queued.filter(isRecallableHumanPrompt).at(-1);
|
|
3155
|
+
const canRecallQueued = recallableQueued !== undefined;
|
|
3156
|
+
const pendingInputVisible = phase === 'ready' &&
|
|
3157
|
+
agentSurface === null && outputViewer === null && jobSurface === null &&
|
|
3158
|
+
permission === null && saveSkillPrompt === null && scheduleReviewPrompt === null &&
|
|
3159
|
+
textPrompt === null && choicePicker === null && copyPicker === null;
|
|
3160
|
+
const pendingInputFurniture = pendingInputVisible
|
|
3161
|
+
? pendingInputPanelRows({
|
|
3162
|
+
pendingSteers: pendingInputItems,
|
|
3163
|
+
queued: queuedInputItems,
|
|
3164
|
+
rows: terminal.rows,
|
|
3165
|
+
canRecall: canRecallQueued,
|
|
3166
|
+
})
|
|
3167
|
+
: 0;
|
|
3059
3168
|
// Where the streaming reply stands among the finalized rows: a row written
|
|
3060
3169
|
// while it streams is drawn below it, as it will be once it has finished.
|
|
3061
3170
|
const streamingAt = messages.findIndex((m) => m.pending);
|
|
@@ -3074,12 +3183,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3074
3183
|
messages,
|
|
3075
3184
|
rows: terminal.rows,
|
|
3076
3185
|
columns: terminal.columns,
|
|
3077
|
-
furnitureRows: LIVE_FURNITURE_ROWS + fullToolFurniture,
|
|
3186
|
+
furnitureRows: LIVE_FURNITURE_ROWS + fullToolFurniture + shellFurniture + pendingInputFurniture,
|
|
3078
3187
|
raw: rawOutput,
|
|
3079
3188
|
});
|
|
3080
3189
|
const liveTasks = checklistShown ? [] : tasks;
|
|
3081
3190
|
const fullTaskFurniture = taskListRows(liveTasks);
|
|
3082
|
-
const compactWork = LIVE_FURNITURE_ROWS + fullTaskFurniture + fullToolFurniture >= terminal.rows;
|
|
3191
|
+
const compactWork = LIVE_FURNITURE_ROWS + fullTaskFurniture + fullToolFurniture + shellFurniture + pendingInputFurniture >= terminal.rows;
|
|
3083
3192
|
const taskFurniture = fullTaskFurniture;
|
|
3084
3193
|
const toolFurniture = compactWork && fullToolFurniture > 0 ? 2 : fullToolFurniture;
|
|
3085
3194
|
// How much of the transcript is still redrawable. The rest belongs to native
|
|
@@ -3097,7 +3206,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3097
3206
|
messages: finalized,
|
|
3098
3207
|
rows: terminal.rows,
|
|
3099
3208
|
columns: terminal.columns,
|
|
3100
|
-
furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
|
|
3209
|
+
furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture + shellFurniture + pendingInputFurniture,
|
|
3101
3210
|
settled: settledRef.current,
|
|
3102
3211
|
raw: rawOutput,
|
|
3103
3212
|
});
|
|
@@ -3106,9 +3215,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3106
3215
|
// settling in the background prints through the child screen. Returning
|
|
3107
3216
|
// advances the floor once and emits those finalized rows exactly once.
|
|
3108
3217
|
const nextSettled = settledBeforeStreaming(messages, window.settled, settledRef.current);
|
|
3109
|
-
if (agentSurface === null && outputViewer === null && scheduleReviewPrompt === null)
|
|
3218
|
+
if (agentSurface === null && outputViewer === null && jobSurface === null && scheduleReviewPrompt === null)
|
|
3110
3219
|
settledRef.current = nextSettled;
|
|
3111
|
-
const renderedSettled = agentSurface === null && outputViewer === null && scheduleReviewPrompt === null
|
|
3220
|
+
const renderedSettled = agentSurface === null && outputViewer === null && jobSurface === null && scheduleReviewPrompt === null
|
|
3112
3221
|
? nextSettled
|
|
3113
3222
|
: settledRef.current;
|
|
3114
3223
|
// One merged vocabulary for the session: this host's own commands plus
|
|
@@ -3478,7 +3587,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3478
3587
|
* leave it hanging with its reply unsaved. It is the same decision Ctrl+C
|
|
3479
3588
|
* sends at that prompt, for the same reason.
|
|
3480
3589
|
*/
|
|
3481
|
-
const interruptTurn = useCallback(() => {
|
|
3590
|
+
const interruptTurn = useCallback((recoverInput = false) => {
|
|
3482
3591
|
const cancelledSwitch = cancelPendingModelSwitch();
|
|
3483
3592
|
for (const pending of permissionQueueRef.current.splice(0)) {
|
|
3484
3593
|
pending.resolve({ kind: 'reject', feedback: 'User interrupted.' });
|
|
@@ -3497,15 +3606,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3497
3606
|
if (activeSessionId)
|
|
3498
3607
|
goalActivation.disarm(activeSessionId);
|
|
3499
3608
|
wakeGoalDriver();
|
|
3500
|
-
|
|
3609
|
+
if (recoverInput && queuedRef.current.length > 0)
|
|
3610
|
+
setQueuePause({ outcome: 'stopped', source: 'operator-interrupt' });
|
|
3611
|
+
else
|
|
3612
|
+
discardQueued();
|
|
3501
3613
|
setState('idle');
|
|
3502
3614
|
return true;
|
|
3503
3615
|
}
|
|
3504
|
-
if (cancelledSwitch)
|
|
3505
|
-
|
|
3616
|
+
if (cancelledSwitch) {
|
|
3617
|
+
if (recoverInput && queuedRef.current.length > 0)
|
|
3618
|
+
setQueuePause({ outcome: 'stopped', source: 'operator-interrupt' });
|
|
3619
|
+
else
|
|
3620
|
+
discardQueued();
|
|
3621
|
+
}
|
|
3506
3622
|
return cancelledSwitch;
|
|
3507
3623
|
}
|
|
3508
3624
|
ac.abort(new TurnCancelled('user'));
|
|
3625
|
+
// Esc with a pending steer means "let this instruction interrupt the
|
|
3626
|
+
// answer". The kernel has not drained these inputs yet, so reclaim them
|
|
3627
|
+
// before closing the inbox and let the queue pump start them first after
|
|
3628
|
+
// the aborted turn settles. Ctrl+C keeps its deliberate cancel-all path.
|
|
3629
|
+
const undeliveredSteers = recoverInput ? (activeTurnInboxRef.current?.close() ?? []) : [];
|
|
3509
3630
|
const activeSessionId = scopeRef.current?.sessionId;
|
|
3510
3631
|
if (activeSessionId)
|
|
3511
3632
|
goalActivation.disarm(activeSessionId);
|
|
@@ -3513,10 +3634,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3513
3634
|
activeTurnTokenRef.current = null;
|
|
3514
3635
|
activeTurnInboxRef.current = null;
|
|
3515
3636
|
setPendingSteers([]);
|
|
3516
|
-
// Dropped now so a second interrupt does not re-abort
|
|
3517
|
-
// it: interrupting means stop, not "run the next one".
|
|
3637
|
+
// Dropped now so a second interrupt does not re-abort.
|
|
3518
3638
|
abortRef.current = null;
|
|
3519
|
-
|
|
3639
|
+
if (recoverInput && undeliveredSteers.length > 0) {
|
|
3640
|
+
replaceQueued([...undeliveredSteers.map((input) => input.prompt), ...queuedRef.current]);
|
|
3641
|
+
setQueuePause(null);
|
|
3642
|
+
}
|
|
3643
|
+
else if (recoverInput && queuedRef.current.length > 0) {
|
|
3644
|
+
// Esc without a pending steer stops the current turn; queued future
|
|
3645
|
+
// instructions stay visible but do not start on a cancelled answer.
|
|
3646
|
+
setQueuePause({ outcome: 'stopped', source: 'operator-interrupt' });
|
|
3647
|
+
}
|
|
3648
|
+
else
|
|
3649
|
+
discardQueued();
|
|
3520
3650
|
clearActiveTools();
|
|
3521
3651
|
setState('idle');
|
|
3522
3652
|
return true;
|
|
@@ -3525,7 +3655,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3525
3655
|
clearActiveTools,
|
|
3526
3656
|
discardQueued,
|
|
3527
3657
|
goalActivation,
|
|
3658
|
+
replaceQueued,
|
|
3528
3659
|
resolvePermission,
|
|
3660
|
+
setQueuePause,
|
|
3529
3661
|
wakeGoalDriver,
|
|
3530
3662
|
]);
|
|
3531
3663
|
// A termination signal leaves like `/exit`, after stopping the turn: the
|
|
@@ -4170,9 +4302,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4170
4302
|
};
|
|
4171
4303
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
4172
4304
|
setSelectedChoice(0);
|
|
4305
|
+
setJobSurface(null);
|
|
4173
4306
|
setChoicePicker(picker);
|
|
4174
4307
|
sendTerminalNotification({ kind: 'approval-required' });
|
|
4175
|
-
}), [closeLiveReply, sendTerminalNotification, setChoicePicker, setSelectedChoice, resolveQuestion]);
|
|
4308
|
+
}), [closeLiveReply, sendTerminalNotification, setChoicePicker, setSelectedChoice, setJobSurface, resolveQuestion]);
|
|
4176
4309
|
const reviewSchedule = useCallback((request, signal) => new Promise((resolve) => {
|
|
4177
4310
|
if (signal?.aborted) {
|
|
4178
4311
|
resolve('cancel');
|
|
@@ -4398,6 +4531,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4398
4531
|
break;
|
|
4399
4532
|
}
|
|
4400
4533
|
case 'tool-end': {
|
|
4534
|
+
// A background bash call creates its job before this event. A job
|
|
4535
|
+
// action may also stop one. Re-read the owning session here so the
|
|
4536
|
+
// shell rail updates while the model continues its turn.
|
|
4537
|
+
if (event.toolName === 'bash' || event.toolName === 'job')
|
|
4538
|
+
setJobSnapshot({ ownerSession: previousSessionRef.current, jobs: previousSessionRef.current?.jobs?.() ?? [] });
|
|
4401
4539
|
const running = activeToolsRef.current;
|
|
4402
4540
|
// Match strictly by toolUseId. Never fall back to "the first
|
|
4403
4541
|
// active tool" — under parallel calls that mis-attributes a
|
|
@@ -4595,9 +4733,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4595
4733
|
break;
|
|
4596
4734
|
}
|
|
4597
4735
|
case 'job':
|
|
4598
|
-
// The
|
|
4599
|
-
//
|
|
4600
|
-
|
|
4736
|
+
// The session listener normally painted this at exit time. A host
|
|
4737
|
+
// without that listener still gets the kernel event; never paint twice.
|
|
4738
|
+
if (!idleJobNoticesRef.current.shown.has(event.jobId)) {
|
|
4739
|
+
idleJobNoticesRef.current.shown.add(event.jobId);
|
|
4740
|
+
pushMessage('system', describeJobExit(event), false, 'J');
|
|
4741
|
+
}
|
|
4601
4742
|
break;
|
|
4602
4743
|
case 'context':
|
|
4603
4744
|
// Into the TRANSCRIPT, not a status line. A status indicator is
|
|
@@ -5070,6 +5211,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5070
5211
|
wakeGoalDriver();
|
|
5071
5212
|
pushMessage('system', `Goal round ${goalRound.round} was not started because this conversation is not durable. Automatic continuation is disarmed; /goal resume retries explicitly.`);
|
|
5072
5213
|
}
|
|
5214
|
+
let idleNoticeSnapshot = { ids: [] };
|
|
5073
5215
|
try {
|
|
5074
5216
|
// Setup above awaited. A conversation switch can happen meanwhile and
|
|
5075
5217
|
// move the mutable SessionScope captured by `createAgentSession`. Re-admit
|
|
@@ -5083,6 +5225,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5083
5225
|
(!goalRound ||
|
|
5084
5226
|
(destination === goalRound.sessionId &&
|
|
5085
5227
|
goalActivation.isArmed(goalRound.sessionId, goalRound)))) {
|
|
5228
|
+
// Keep these notices pending until a completed send commits them.
|
|
5229
|
+
// An early throw or abort never proves the model received context.
|
|
5230
|
+
idleNoticeSnapshot = snapshotIdleJobNotices();
|
|
5231
|
+
const operatorShellSnapshot = drainOperatorShell();
|
|
5086
5232
|
for await (const event of session.send(priorForSdk, {
|
|
5087
5233
|
signal: ac.signal,
|
|
5088
5234
|
turnId,
|
|
@@ -5103,7 +5249,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5103
5249
|
...(turnHypermode ? { hypermode: true } : {}),
|
|
5104
5250
|
// Read at every iteration, so an after-turn trigger a steer
|
|
5105
5251
|
// binds to this turn is said from the next one on.
|
|
5106
|
-
hostContext: () =>
|
|
5252
|
+
hostContext: () => [
|
|
5253
|
+
...(st.contextTexts ?? []),
|
|
5254
|
+
idleNoticeSnapshot.text,
|
|
5255
|
+
operatorShellSnapshot,
|
|
5256
|
+
].filter((part) => Boolean(part)),
|
|
5107
5257
|
...(goalRound ? { goalRound } : {}),
|
|
5108
5258
|
// The mode above decides whether this callback is consulted.
|
|
5109
5259
|
onPermission: askPermission,
|
|
@@ -5111,9 +5261,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5111
5261
|
onModelSwitch: requestModelSwitch,
|
|
5112
5262
|
inboundMessages: () => inbox.drain(),
|
|
5113
5263
|
waitForInbound: (signal) => inbox.waitForInbound(signal),
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
.
|
|
5264
|
+
onJobNoticeDelivered: (ids) => {
|
|
5265
|
+
const notices = idleJobNoticesRef.current;
|
|
5266
|
+
if (notices.ownerSession !== session)
|
|
5267
|
+
return;
|
|
5268
|
+
for (const id of ids) {
|
|
5269
|
+
notices.delivered.add(id);
|
|
5270
|
+
notices.pending.delete(id);
|
|
5271
|
+
}
|
|
5272
|
+
},
|
|
5273
|
+
extraSystem: composeSkillsPrompt(activeSkills) || undefined,
|
|
5117
5274
|
onConversationMessages: (messages) => {
|
|
5118
5275
|
// State-only: opaque reasoning/signatures must reach the next
|
|
5119
5276
|
// provider and durable store without becoming transcript text.
|
|
@@ -5167,6 +5324,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5167
5324
|
}
|
|
5168
5325
|
}
|
|
5169
5326
|
finally {
|
|
5327
|
+
if (st.completed && idleJobNoticesRef.current.ownerSession === session) {
|
|
5328
|
+
for (const id of idleNoticeSnapshot.ids)
|
|
5329
|
+
idleJobNoticesRef.current.pending.delete(id);
|
|
5330
|
+
}
|
|
5170
5331
|
const undeliveredLiveInput = inbox.close();
|
|
5171
5332
|
const ownsInbox = activeTurnInboxRef.current === inbox;
|
|
5172
5333
|
if (ownsInbox) {
|
|
@@ -5240,6 +5401,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5240
5401
|
// The kernel recorded the turn in the session log as it ran; nothing is
|
|
5241
5402
|
// left to publish once the loop has unwound.
|
|
5242
5403
|
unsettledTurnGenerationsRef.current.delete(turnToken);
|
|
5404
|
+
if (queuedRef.current.length > 0)
|
|
5405
|
+
setQueueSettleVersion((version) => version + 1);
|
|
5243
5406
|
const pending = pendingModelSwitchRef.current;
|
|
5244
5407
|
if (pending?.turnToken === turnToken && pending.selection) {
|
|
5245
5408
|
const selection = pending.selection;
|
|
@@ -5611,6 +5774,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5611
5774
|
}, () => pushMessage('system', renderAgents(agentIds)));
|
|
5612
5775
|
return;
|
|
5613
5776
|
}
|
|
5777
|
+
case 'jobs': {
|
|
5778
|
+
const current = session?.jobs?.() ?? [];
|
|
5779
|
+
if (!session) {
|
|
5780
|
+
pushMessage('system', 'Shell jobs are available after a session starts.');
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
setJobSnapshot({ ownerSession: session, jobs: current });
|
|
5784
|
+
setJobClock(Date.now());
|
|
5785
|
+
setJobSurface({ ownerSession: session, selectedJobId: current.find((job) => job.status === 'running')?.id ?? current[0]?.id ?? null, detailJobId: null, tailOffset: 0 });
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
5614
5788
|
case 'abandon': {
|
|
5615
5789
|
void abandonActiveTurn(slash.reason);
|
|
5616
5790
|
return;
|
|
@@ -6631,10 +6805,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6631
6805
|
//
|
|
6632
6806
|
// Keep the attachment array beside its text. Reconstructing a prompt from
|
|
6633
6807
|
// the transcript later cannot recover bytes whose composer chip is gone.
|
|
6634
|
-
|
|
6808
|
+
const wasHeldByInterrupt = queuePauseRef.current?.source === 'operator-interrupt';
|
|
6809
|
+
if (mode === 'submit')
|
|
6810
|
+
advanceQueueContinuation();
|
|
6635
6811
|
const prompt = {
|
|
6636
6812
|
kind: 'human',
|
|
6637
6813
|
text: outgoing,
|
|
6814
|
+
...(meta.source === 'composer' && !skillFlow ? { recallable: true } : {}),
|
|
6815
|
+
...(meta.source === 'composer' && !skillFlow && outgoing !== value ? { recallText: value } : {}),
|
|
6638
6816
|
...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
|
|
6639
6817
|
...(skillFlow ? { skillFlow: true } : {}),
|
|
6640
6818
|
...(armedTriggers.length > 0 && !skillFlow ? { triggers: armedTriggers } : {}),
|
|
@@ -6654,14 +6832,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6654
6832
|
prompt,
|
|
6655
6833
|
message: createUserMessage(expanded.sendText, prompt.attachments),
|
|
6656
6834
|
attachedFiles: expanded.attached.length,
|
|
6657
|
-
|
|
6835
|
+
queuedBefore: [...queuedRef.current],
|
|
6658
6836
|
})) {
|
|
6659
6837
|
bindSteeredTriggers(armedTriggers);
|
|
6660
6838
|
return;
|
|
6661
6839
|
}
|
|
6662
6840
|
}
|
|
6663
6841
|
}
|
|
6664
|
-
|
|
6842
|
+
if (mode === 'submit' && wasHeldByInterrupt)
|
|
6843
|
+
replaceQueued([prompt, ...queuedRef.current]);
|
|
6844
|
+
else
|
|
6845
|
+
enqueueQueued(prompt);
|
|
6665
6846
|
}, [
|
|
6666
6847
|
activeSkills,
|
|
6667
6848
|
advanceQueueContinuation,
|
|
@@ -6689,6 +6870,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6689
6870
|
rawOutput,
|
|
6690
6871
|
rememberProjectNote,
|
|
6691
6872
|
removeStoredCredential,
|
|
6873
|
+
replaceQueued,
|
|
6692
6874
|
resetTranscript,
|
|
6693
6875
|
runConversationExport,
|
|
6694
6876
|
session,
|
|
@@ -6975,6 +7157,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6975
7157
|
state,
|
|
6976
7158
|
phase,
|
|
6977
7159
|
queued,
|
|
7160
|
+
queueSettleVersion,
|
|
6978
7161
|
modelSwitchVersion,
|
|
6979
7162
|
queuePause,
|
|
6980
7163
|
textPrompt,
|
|
@@ -7296,6 +7479,110 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7296
7479
|
resolvePermission({ kind });
|
|
7297
7480
|
return;
|
|
7298
7481
|
}
|
|
7482
|
+
// The shell view observes only this AgentSession's jobs. Consent always
|
|
7483
|
+
// takes precedence above; the composer stays mounted but hidden below.
|
|
7484
|
+
const shellView = jobSurfaceRef.current;
|
|
7485
|
+
if (shellView) {
|
|
7486
|
+
if (shellView.ownerSession !== session) {
|
|
7487
|
+
setJobSurface(null);
|
|
7488
|
+
return;
|
|
7489
|
+
}
|
|
7490
|
+
// The Return that submitted /jobs must not also inspect or stop a
|
|
7491
|
+
// row before the new screen has painted.
|
|
7492
|
+
if (jobSurfaceCommittedRef.current !== shellView)
|
|
7493
|
+
return;
|
|
7494
|
+
if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
7495
|
+
setJobSurface(null);
|
|
7496
|
+
return;
|
|
7497
|
+
}
|
|
7498
|
+
if (key.escape) {
|
|
7499
|
+
setJobSurface(shellView.detailJobId ? { ...shellView, detailJobId: null, tailOffset: 0 } : null);
|
|
7500
|
+
return;
|
|
7501
|
+
}
|
|
7502
|
+
const selectedId = shellView.detailJobId ?? shellView.selectedJobId ?? backgroundJobs[0]?.id;
|
|
7503
|
+
const selectedJob = backgroundJobs.find((job) => job.id === selectedId);
|
|
7504
|
+
if (input.toLowerCase() === 'x' && selectedJob?.status === 'running' && session?.stopJob && !shellView.busyJobId) {
|
|
7505
|
+
const owner = session;
|
|
7506
|
+
const stopJob = owner.stopJob;
|
|
7507
|
+
if (!stopJob)
|
|
7508
|
+
return;
|
|
7509
|
+
setJobSurface({ ...shellView, busyJobId: selectedJob.id, notice: undefined });
|
|
7510
|
+
void stopJob(selectedJob.id).then((result) => {
|
|
7511
|
+
if (previousSessionRef.current !== owner)
|
|
7512
|
+
return;
|
|
7513
|
+
setJobSnapshot({ ownerSession: owner, jobs: owner.jobs?.() ?? [] });
|
|
7514
|
+
const current = jobSurfaceRef.current;
|
|
7515
|
+
if (current?.busyJobId === selectedJob.id)
|
|
7516
|
+
setJobSurface({
|
|
7517
|
+
...current,
|
|
7518
|
+
busyJobId: undefined,
|
|
7519
|
+
notice: result.status === 'killed'
|
|
7520
|
+
? `Stopped ${selectedJob.id}.`
|
|
7521
|
+
: result.status === 'exited'
|
|
7522
|
+
? `${selectedJob.id} had already exited (${result.exitCode ?? '?'}).`
|
|
7523
|
+
: `Stop request for ${selectedJob.id} did not finish.`,
|
|
7524
|
+
});
|
|
7525
|
+
}, (error) => {
|
|
7526
|
+
if (previousSessionRef.current !== owner)
|
|
7527
|
+
return;
|
|
7528
|
+
const current = jobSurfaceRef.current;
|
|
7529
|
+
if (current?.busyJobId === selectedJob.id)
|
|
7530
|
+
setJobSurface({ ...current, busyJobId: undefined, notice: `Could not stop ${selectedJob.id}: ${error instanceof Error ? error.message : String(error)}` });
|
|
7531
|
+
});
|
|
7532
|
+
return;
|
|
7533
|
+
}
|
|
7534
|
+
if (shellView.detailJobId) {
|
|
7535
|
+
let output;
|
|
7536
|
+
try {
|
|
7537
|
+
output = session?.readJob?.(shellView.detailJobId);
|
|
7538
|
+
}
|
|
7539
|
+
catch {
|
|
7540
|
+
output = undefined;
|
|
7541
|
+
}
|
|
7542
|
+
const max = maxBackgroundJobTailOffset(output, terminal.rows, Math.max(1, terminal.columns - 2));
|
|
7543
|
+
const page = backgroundJobOutputPageSize(terminal.rows);
|
|
7544
|
+
const offset = Math.min(shellView.tailOffset, max);
|
|
7545
|
+
const next = key.home || input === 'g'
|
|
7546
|
+
? max
|
|
7547
|
+
: key.end || input === 'G'
|
|
7548
|
+
? 0
|
|
7549
|
+
: key.pageUp
|
|
7550
|
+
? Math.min(max, offset + page)
|
|
7551
|
+
: key.pageDown
|
|
7552
|
+
? Math.max(0, offset - page)
|
|
7553
|
+
: key.upArrow
|
|
7554
|
+
? Math.min(max, offset + 1)
|
|
7555
|
+
: key.downArrow
|
|
7556
|
+
? Math.max(0, offset - 1)
|
|
7557
|
+
: null;
|
|
7558
|
+
if (next !== null)
|
|
7559
|
+
setJobSurface({ ...shellView, tailOffset: next, notice: undefined });
|
|
7560
|
+
return;
|
|
7561
|
+
}
|
|
7562
|
+
if (key.return && selectedJob) {
|
|
7563
|
+
setJobSurface({ ...shellView, selectedJobId: selectedJob.id, detailJobId: selectedJob.id, tailOffset: 0, notice: undefined });
|
|
7564
|
+
return;
|
|
7565
|
+
}
|
|
7566
|
+
if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
|
|
7567
|
+
const current = Math.max(0, backgroundJobs.findIndex((job) => job.id === shellView.selectedJobId));
|
|
7568
|
+
const page = backgroundJobsPageSize(terminal.rows);
|
|
7569
|
+
const next = key.home
|
|
7570
|
+
? 0
|
|
7571
|
+
: key.end
|
|
7572
|
+
? backgroundJobs.length - 1
|
|
7573
|
+
: key.pageUp
|
|
7574
|
+
? Math.max(0, current - page)
|
|
7575
|
+
: key.pageDown
|
|
7576
|
+
? Math.min(backgroundJobs.length - 1, current + page)
|
|
7577
|
+
: key.upArrow
|
|
7578
|
+
? Math.max(0, current - 1)
|
|
7579
|
+
: Math.min(backgroundJobs.length - 1, current + 1);
|
|
7580
|
+
const job = backgroundJobs[next];
|
|
7581
|
+
if (job)
|
|
7582
|
+
setJobSurface({ ...shellView, selectedJobId: job.id, notice: undefined });
|
|
7583
|
+
}
|
|
7584
|
+
return;
|
|
7585
|
+
}
|
|
7299
7586
|
// The active child count already advertises this key beside Working.
|
|
7300
7587
|
// Open the same retained cockpit as `/agent`; Enter then drills into a
|
|
7301
7588
|
// child's transcript. Keep the shortcut below consent so it can never
|
|
@@ -7614,10 +7901,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7614
7901
|
void abandonActiveTurn('The operator stopped the turn a tool paused for them.');
|
|
7615
7902
|
return;
|
|
7616
7903
|
}
|
|
7617
|
-
// Esc interrupts
|
|
7618
|
-
//
|
|
7904
|
+
// Esc interrupts the running turn. Its pending steer can start a fresh
|
|
7905
|
+
// turn, while a next-turn queue remains visible for the operator.
|
|
7619
7906
|
if (key.escape && (abortRef.current || pendingModelSwitchRef.current)) {
|
|
7620
|
-
interruptTurn();
|
|
7907
|
+
interruptTurn(true);
|
|
7621
7908
|
pushMessage('system', 'Interrupted.');
|
|
7622
7909
|
return;
|
|
7623
7910
|
}
|
|
@@ -7651,7 +7938,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7651
7938
|
return;
|
|
7652
7939
|
}
|
|
7653
7940
|
const projected = liveWindow({ messages: proposed.filter((m) => !m.pending), rows: terminal.rows,
|
|
7654
|
-
columns: terminal.columns, furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
|
|
7941
|
+
columns: terminal.columns, furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture + shellFurniture + pendingInputFurniture,
|
|
7655
7942
|
settled: settledRef.current, raw: rawOutput });
|
|
7656
7943
|
if (collapsible.length === 0 || (expanding && projected.settled > settledRef.current)) {
|
|
7657
7944
|
setOutputViewer(block);
|
|
@@ -7707,6 +7994,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7707
7994
|
? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
|
|
7708
7995
|
: undefined;
|
|
7709
7996
|
const lifecycleOwnsViewport = outputViewer !== null ||
|
|
7997
|
+
jobSurface !== null ||
|
|
7710
7998
|
scheduleReviewPrompt !== null ||
|
|
7711
7999
|
phase === 'trust' ||
|
|
7712
8000
|
phase === 'resume' ||
|
|
@@ -7723,7 +8011,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7723
8011
|
choicePicker === null &&
|
|
7724
8012
|
copyPicker === null &&
|
|
7725
8013
|
scheduleReviewPrompt === null &&
|
|
7726
|
-
agentSurface === null
|
|
8014
|
+
agentSurface === null &&
|
|
8015
|
+
jobSurface === null
|
|
7727
8016
|
? goalStatusLabel(goalStatus, goalStatus ? goalActivation.isArmed(goalStatus.sessionId, goalStatus) : false)
|
|
7728
8017
|
: null;
|
|
7729
8018
|
// The effort chooser is a left-to-right slider wherever its stops fit on
|
|
@@ -7731,6 +8020,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7731
8020
|
const effortSlider = choicePicker?.kind === 'reasoning-effort'
|
|
7732
8021
|
? effortSliderLayout(choicePicker.options.map((option) => option.label), Math.max(1, (terminal.columns ?? 80) - 2))
|
|
7733
8022
|
: undefined;
|
|
8023
|
+
const jobStatusHint = jobSurface?.detailJobId
|
|
8024
|
+
? !jobSurface.busyJobId &&
|
|
8025
|
+
backgroundJobs.some((job) => job.id === jobSurface.detailJobId && job.status === 'running')
|
|
8026
|
+
? 'shell output · x stop · esc list'
|
|
8027
|
+
: 'shell output · esc list'
|
|
8028
|
+
: 'shells · enter inspect · esc close';
|
|
7734
8029
|
const statusHint = conversationMutation === 'fork'
|
|
7735
8030
|
? 'forking conversation — input is paused'
|
|
7736
8031
|
: conversationMutation === 'edit'
|
|
@@ -7755,21 +8050,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7755
8050
|
: 'agents — enter inspect · left phases · esc return'
|
|
7756
8051
|
: agentSurface?.kind === 'transcript'
|
|
7757
8052
|
? 'observing agent — esc agents · q parent'
|
|
7758
|
-
:
|
|
7759
|
-
?
|
|
7760
|
-
:
|
|
7761
|
-
?
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
? '
|
|
7765
|
-
:
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
:
|
|
7769
|
-
? '
|
|
7770
|
-
:
|
|
7771
|
-
?
|
|
7772
|
-
:
|
|
8053
|
+
: jobSurface
|
|
8054
|
+
? jobStatusHint
|
|
8055
|
+
: textPrompt
|
|
8056
|
+
? 'editor open · enter apply · esc cancel'
|
|
8057
|
+
: choicePicker
|
|
8058
|
+
? choicePickerSearchable(choicePicker)
|
|
8059
|
+
? 'type to filter · ↑↓ select · enter apply · esc back'
|
|
8060
|
+
: effortSlider
|
|
8061
|
+
? '←/→ adjust · 1–9 select · enter apply · esc back'
|
|
8062
|
+
: '↑↓ / 1–9 select · enter apply · esc back'
|
|
8063
|
+
: copyPicker
|
|
8064
|
+
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
8065
|
+
: handoffPark && phase === 'ready' && state === 'idle' && !composerHasDraft
|
|
8066
|
+
? 'enter continue · esc stop'
|
|
8067
|
+
: goalStatus && phase === 'ready' && state === 'idle'
|
|
8068
|
+
? undefined
|
|
8069
|
+
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
7773
8070
|
const permissionSource = reviewSource(permission, scopeRef.current?.sessionId);
|
|
7774
8071
|
const permissionOwner = permissionSource
|
|
7775
8072
|
? subagents.find((agent) => agent.sessionId === permissionSource)
|
|
@@ -7809,6 +8106,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7809
8106
|
phase !== 'resume' &&
|
|
7810
8107
|
phase !== 'edit' &&
|
|
7811
8108
|
phase !== 'picker';
|
|
8109
|
+
const childSurfaceOpen = agentSurface !== null || outputViewer !== null || jobSurface !== null;
|
|
7812
8110
|
return (
|
|
7813
8111
|
// One row short of the terminal, always: see ViewportBound.tsx for the
|
|
7814
8112
|
// renderer path a taller frame takes and the scrollback row it costs.
|
|
@@ -7826,7 +8124,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7826
8124
|
// where it is decided rather than guessed on the screen: with no
|
|
7827
8125
|
// session behind the picker, esc exits namzu and the footer says
|
|
7828
8126
|
// so instead of offering a cancel nobody will see.
|
|
7829
|
-
cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null && scheduleReviewPrompt === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true &&
|
|
8127
|
+
cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null && jobSurface === null && scheduleReviewPrompt === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true &&
|
|
7830
8128
|
permission === null &&
|
|
7831
8129
|
textPrompt === null &&
|
|
7832
8130
|
saveSkillPrompt === null &&
|
|
@@ -7834,13 +8132,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7834
8132
|
permission !== null ||
|
|
7835
8133
|
textPrompt !== null ||
|
|
7836
8134
|
saveSkillPrompt !== null ||
|
|
7837
|
-
scheduleReviewPrompt !== null })) : null, agentSurface === null && outputViewer === null && permission === null && scheduleReviewPrompt === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows, batchOnly: permission.batchOnly === true, siteNotes: permission.siteNotes, ...(permission.screenConsent === true
|
|
8135
|
+
scheduleReviewPrompt !== null })) : null, agentSurface === null && outputViewer === null && jobSurface === null && permission === null && scheduleReviewPrompt === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows, batchOnly: permission.batchOnly === true, siteNotes: permission.siteNotes, ...(permission.screenConsent === true
|
|
7838
8136
|
? { screenConsent: { provider: session?.providerSummary ?? null } }
|
|
7839
|
-
: {}) })) : null, saveSkillPrompt && permission === null && scheduleReviewPrompt === null ? (_jsx(SaveSkillOverlay, { request: saveSkillPrompt.request, cwd: ctx.cwd, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: terminal.rows, onAnswer: saveSkillPrompt.resolve }, saveSkillPrompt.request.markdown)) : null, scheduleReviewPrompt && permission === null ? (_jsx(ScheduleReviewOverlay, { request: scheduleReviewPrompt.request, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: Math.max(1, (terminal.rows ?? 24) - 2), onAnswer: scheduleReviewPrompt.resolve }, scheduleReviewPrompt.token)) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || scheduleReviewPrompt !== null ||
|
|
7840
|
-
|
|
7841
|
-
outputViewer === null &&
|
|
8137
|
+
: {}) })) : null, saveSkillPrompt && permission === null && scheduleReviewPrompt === null ? (_jsx(SaveSkillOverlay, { request: saveSkillPrompt.request, cwd: ctx.cwd, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: terminal.rows, onAnswer: saveSkillPrompt.resolve }, saveSkillPrompt.request.markdown)) : null, scheduleReviewPrompt && permission === null ? (_jsx(ScheduleReviewOverlay, { request: scheduleReviewPrompt.request, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: Math.max(1, (terminal.rows ?? 24) - 2), onAnswer: scheduleReviewPrompt.resolve }, scheduleReviewPrompt.token)) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || scheduleReviewPrompt !== null || childSurfaceOpen, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && scheduleReviewPrompt === null &&
|
|
8138
|
+
!childSurfaceOpen &&
|
|
7842
8139
|
choicePicker?.kind === 'reasoning-effort' &&
|
|
7843
|
-
effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : permission === null && scheduleReviewPrompt === null &&
|
|
8140
|
+
effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : permission === null && scheduleReviewPrompt === null && !childSurfaceOpen && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && scheduleReviewPrompt === null && !childSurfaceOpen && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { columns: Math.max(2, (terminal.columns ?? 80) - 2), working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, ...(hypermode ? { mode: HYPERMODE } : {}), activation: hypermodeActivation, animate: externalEditorRequest === null, focus: phase === 'ready' &&
|
|
7844
8141
|
state !== 'awaiting-permission' &&
|
|
7845
8142
|
!compacting &&
|
|
7846
8143
|
externalEditorRequest === null &&
|
|
@@ -7850,55 +8147,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7850
8147
|
copyPicker === null &&
|
|
7851
8148
|
saveSkillPrompt === null &&
|
|
7852
8149
|
scheduleReviewPrompt === null &&
|
|
7853
|
-
|
|
8150
|
+
!childSurfaceOpen, hidden: permission !== null ||
|
|
7854
8151
|
saveSkillPrompt !== null ||
|
|
7855
8152
|
scheduleReviewPrompt !== null ||
|
|
7856
8153
|
textPrompt !== null ||
|
|
7857
8154
|
choicePicker !== null ||
|
|
7858
8155
|
copyPicker !== null ||
|
|
7859
|
-
(
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
copyPicker === null &&
|
|
7864
|
-
agentSurface === null && outputViewer === 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 &&
|
|
7865
|
-
permission === null &&
|
|
7866
|
-
textPrompt === null &&
|
|
7867
|
-
choicePicker === null &&
|
|
7868
|
-
copyPicker === null &&
|
|
7869
|
-
agentSurface === null && outputViewer === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '‖' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
7870
|
-
? queuePause.outcome === 'paused'
|
|
7871
|
-
? 'held after a resumable turn paused; wait for recovery, change model, or send a message to release it'
|
|
7872
|
-
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
7873
|
-
: 'sending when ready', queuedTriggerLabels(queued)] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
8156
|
+
childSurfaceOpen, children: [pendingInputVisible ? (_jsx(PendingInputPanel, { pendingSteers: pendingInputItems, queued: queuedInputItems, columns: Math.max(2, (terminal.columns ?? 80) - 2), rows: terminal.rows, canRecall: canRecallQueued, ...(recallableQueued
|
|
8157
|
+
? { recallItem: { text: recallableQueued.recallText ?? recallableQueued.text, attachmentCount: recallableQueued.attachments?.length } }
|
|
8158
|
+
: {}), queuedTags: queuedTriggerLabels(queued), held: queuePause })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
8159
|
+
jobSurface !== null ||
|
|
7874
8160
|
phase !== 'ready' ||
|
|
7875
8161
|
state === 'awaiting-permission' ||
|
|
7876
8162
|
compacting ||
|
|
7877
8163
|
conversationMutation !== null ||
|
|
7878
8164
|
externalEditorRequest !== null ||
|
|
7879
|
-
|
|
8165
|
+
childSurfaceOpen, hidden: permission !== null ||
|
|
7880
8166
|
saveSkillPrompt !== null ||
|
|
7881
8167
|
scheduleReviewPrompt !== null ||
|
|
7882
8168
|
textPrompt !== null ||
|
|
7883
8169
|
choicePicker !== null ||
|
|
7884
8170
|
copyPicker !== null ||
|
|
7885
|
-
|
|
8171
|
+
childSurfaceOpen,
|
|
7886
8172
|
// A turn is running, so Esc is the interrupt and not
|
|
7887
8173
|
// the composer's clear.
|
|
7888
8174
|
escapeInterrupts: (!compacting &&
|
|
7889
8175
|
(state === 'thinking' ||
|
|
7890
8176
|
state === 'tool' ||
|
|
7891
8177
|
pendingModelSwitchRef.current !== null)) ||
|
|
7892
|
-
(handoffPark !== null && state === 'idle'), 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) => ({
|
|
8178
|
+
(handoffPark !== null && state === 'idle'), 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, onRecallQueued: recallQueuedPrompt, userCommands: userCommands, builtins: hostCommands.map((command) => ({
|
|
7893
8179
|
...command,
|
|
7894
8180
|
description: command.unavailable?.(slashCtx) ?? command.description,
|
|
7895
|
-
})), mentionCandidates: mentionCandidates, history: history, ...(composerTriggers ? { triggers: composerTriggers } : {}), turnActive: !compacting && (state === 'thinking' || state === 'tool') })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
|
|
8181
|
+
})), mentionCandidates: mentionCandidates, history: history, ...(composerTriggers ? { triggers: composerTriggers } : {}), turnActive: !compacting && (state === 'thinking' || state === 'tool') })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface && !childSurfaceOpen && permission === null && scheduleReviewPrompt === null && runningShellCount > 0 ? (_jsx(Box, { children: _jsx(Text, { color: theme.text.secondary, wrap: "truncate-end", children: terminal.columns < 38
|
|
8182
|
+
? `${runningShellCount} shell${runningShellCount === 1 ? '' : 's'} · /jobs`
|
|
8183
|
+
: `${runningShellCount} shell${runningShellCount === 1 ? '' : 's'} running · /jobs to manage` }) })) : null, showComposerSurface &&
|
|
7896
8184
|
permission === null &&
|
|
7897
8185
|
agentSurface === null &&
|
|
7898
8186
|
outputViewer === null &&
|
|
8187
|
+
jobSurface === null &&
|
|
7899
8188
|
scheduleReviewPrompt === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
|
|
7900
8189
|
agentSurface === null &&
|
|
7901
8190
|
outputViewer === null &&
|
|
8191
|
+
jobSurface === null &&
|
|
7902
8192
|
scheduleReviewPrompt === null &&
|
|
7903
8193
|
liveSubagents.length > 0 ? (
|
|
7904
8194
|
// The active agents remain in memory while the schedule review
|
|
@@ -7913,7 +8203,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7913
8203
|
}, onNext: () => {
|
|
7914
8204
|
if (outputIndex >= 0 && outputIndex + 1 < outputBlocks.length)
|
|
7915
8205
|
setOutputViewer(outputBlocks[outputIndex + 1]);
|
|
7916
|
-
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null] }) }));
|
|
8206
|
+
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null, jobSurface && permission === null && scheduleReviewPrompt === null ? (_jsx(BackgroundJobsPanel, { jobs: backgroundJobs, selectedJobId: jobSurface.selectedJobId, detailJobId: jobSurface.detailJobId, tailOffset: jobSurface.tailOffset, readJob: (id) => session?.readJob?.(id), rows: terminal.rows, columns: Math.max(1, terminal.columns - 2), now: jobClock, busyJobId: jobSurface.busyJobId, notice: jobSurface.notice })) : null] }) }));
|
|
7917
8207
|
}
|
|
7918
8208
|
/** One line for a background job that ended, for the transcript and the next turn. */
|
|
7919
8209
|
function describeJobExit(job) {
|