@namzu/cli 34.1.1 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/commands/mcp.d.ts +3 -0
  3. package/dist/commands/mcp.d.ts.map +1 -1
  4. package/dist/commands/mcp.js +179 -6
  5. package/dist/commands/mcp.js.map +1 -1
  6. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  7. package/dist/integrations/mcp/servers.js +107 -15
  8. package/dist/integrations/mcp/servers.js.map +1 -1
  9. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  10. package/dist/integrations/resident/session-step.js +13 -8
  11. package/dist/integrations/resident/session-step.js.map +1 -1
  12. package/dist/tui/App.d.ts.map +1 -1
  13. package/dist/tui/App.js +385 -95
  14. package/dist/tui/App.js.map +1 -1
  15. package/dist/tui/BackgroundJobsPanel.d.ts +26 -0
  16. package/dist/tui/BackgroundJobsPanel.d.ts.map +1 -0
  17. package/dist/tui/BackgroundJobsPanel.js +129 -0
  18. package/dist/tui/BackgroundJobsPanel.js.map +1 -0
  19. package/dist/tui/Composer.d.ts +7 -2
  20. package/dist/tui/Composer.d.ts.map +1 -1
  21. package/dist/tui/Composer.js +65 -34
  22. package/dist/tui/Composer.js.map +1 -1
  23. package/dist/tui/PendingInputPanel.d.ts +34 -0
  24. package/dist/tui/PendingInputPanel.d.ts.map +1 -0
  25. package/dist/tui/PendingInputPanel.js +108 -0
  26. package/dist/tui/PendingInputPanel.js.map +1 -0
  27. package/dist/tui/agent.d.ts +10 -3
  28. package/dist/tui/agent.d.ts.map +1 -1
  29. package/dist/tui/agent.js +78 -12
  30. package/dist/tui/agent.js.map +1 -1
  31. package/dist/tui/slashCommands.d.ts +5 -1
  32. package/dist/tui/slashCommands.d.ts.map +1 -1
  33. package/dist/tui/slashCommands.js +9 -8
  34. package/dist/tui/slashCommands.js.map +1 -1
  35. 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 inserted = 0;
240
+ let lastInsertedIndex = -1;
236
241
  for (const input of undelivered) {
237
- const index = Math.min(input.queueBoundary + inserted, merged.length);
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
- inserted += 1;
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
- // Jobs that ended while no turn was running.
455
- //
456
- // Only those: an exit that lands mid-turn belongs to the kernel, which
457
- // rides it out on the next tool result or, for a job the model awaited,
458
- // delivers it as the message that releases its own suspend. Queueing one
459
- // here as well would announce the same exit twice, so the `abortRef`
460
- // check below is the boundary between the two owners rather than an
461
- // optimisation. Between turns the kernel is not listening, so the exit is
462
- // held here and handed to the next send as part of the system text; the
463
- // transcript row is written at once.
464
- const idleJobNoticesRef = useRef([]);
465
- const drainIdleJobNotices = () => {
466
- if (idleJobNoticesRef.current.length === 0)
467
- return undefined;
468
- const lines = idleJobNoticesRef.current;
469
- idleJobNoticesRef.current = [];
470
- return `Background jobs that ended since your last turn:\n${lines.map((l) => `- ${l}`).join('\n')}`;
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
- if (abortRef.current !== null)
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
- idleJobNoticesRef.current.push(text);
496
- pushMessage('system', text, false, 'J');
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
- discardQueued();
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
- discardQueued();
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, and the queue with
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
- discardQueued();
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 kernel saw it end during a turn; the model reads the notice
4599
- // on its next tool result, the operator reads this row.
4600
- pushMessage('system', describeJobExit(event), false, 'J');
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: () => st.contextTexts ?? [],
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
- extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
5115
- .filter((part) => Boolean(part))
5116
- .join('\n\n') || undefined,
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
- advanceQueueContinuation();
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
- queueBoundary: queuedRef.current.length,
6835
+ queuedBefore: [...queuedRef.current],
6658
6836
  })) {
6659
6837
  bindSteeredTriggers(armedTriggers);
6660
6838
  return;
6661
6839
  }
6662
6840
  }
6663
6841
  }
6664
- enqueueQueued(prompt);
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 a running turn (Ctrl+C stays reserved for exit). Mirrors
7618
- // the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
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
- : textPrompt
7759
- ? 'editor open · enter apply · esc cancel'
7760
- : choicePicker
7761
- ? choicePickerSearchable(choicePicker)
7762
- ? 'type to filter · ↑↓ select · enter apply · esc back'
7763
- : effortSlider
7764
- ? '←/→ adjust · 1–9 select · enter apply · esc back'
7765
- : '↑↓ / 1–9 select · enter apply · esc back'
7766
- : copyPicker
7767
- ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
7768
- : handoffPark && phase === 'ready' && state === 'idle' && !composerHasDraft
7769
- ? 'enter continue · esc stop'
7770
- : goalStatus && phase === 'ready' && state === 'idle'
7771
- ? undefined
7772
- : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
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 || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && scheduleReviewPrompt === null &&
7840
- agentSurface === null &&
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 && agentSurface === null && outputViewer === null && 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 && agentSurface === null && outputViewer === null && 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' &&
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
- agentSurface === null && outputViewer === null, hidden: permission !== null ||
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
- (agentSurface !== null || outputViewer !== null), children: [pendingSteers.length > 0 &&
7860
- permission === null &&
7861
- textPrompt === null &&
7862
- choicePicker === null &&
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
- (agentSurface !== null || outputViewer !== null), hidden: permission !== null ||
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
- (agentSurface !== null || outputViewer !== null),
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) {