gaunt-sloth 2.0.0-beta.8 → 2.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/.gsloth.review-discovery.md +19 -0
- package/cli.js +49 -0
- package/dist/cli.js +37 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/adkEvalRunner.d.ts +7 -6
- package/dist/commands/adkEvalRunner.js +3 -3
- package/dist/commands/adkEvalRunner.js.map +1 -1
- package/dist/commands/agUiEvalRunner.d.ts +4 -4
- package/dist/commands/agUiEvalRunner.js +8 -7
- package/dist/commands/agUiEvalRunner.js.map +1 -1
- package/dist/commands/askCommand.js +9 -1
- package/dist/commands/askCommand.js.map +1 -1
- package/dist/commands/batchCommand.d.ts +2 -2
- package/dist/commands/batchCommand.js +10 -5
- package/dist/commands/batchCommand.js.map +1 -1
- package/dist/commands/commandIntrospection.d.ts +1 -1
- package/dist/commands/commandIntrospection.js +6 -3
- package/dist/commands/commandIntrospection.js.map +1 -1
- package/dist/commands/configCommand.d.ts +1 -1
- package/dist/commands/configCommand.js +1 -1
- package/dist/commands/configSetup.d.ts +1 -1
- package/dist/commands/discoveryConfigSchemaAgreement.d.ts +45 -0
- package/dist/commands/discoveryConfigSchemaAgreement.js +2 -0
- package/dist/commands/discoveryConfigSchemaAgreement.js.map +1 -0
- package/dist/commands/evalCommand.d.ts +27 -1
- package/dist/commands/evalCommand.js +155 -13
- package/dist/commands/evalCommand.js.map +1 -1
- package/dist/commands/execCommand.d.ts +1 -1
- package/dist/commands/execCommand.js +9 -2
- package/dist/commands/execCommand.js.map +1 -1
- package/dist/commands/firstRunDialog.d.ts +3 -3
- package/dist/commands/firstRunDialog.js +2 -2
- package/dist/commands/getCommand.js +10 -1
- package/dist/commands/getCommand.js.map +1 -1
- package/dist/commands/historyCommand.d.ts +1 -1
- package/dist/commands/historyCommand.js +16 -4
- package/dist/commands/historyCommand.js.map +1 -1
- package/dist/commands/prCommand.js +45 -16
- package/dist/commands/prCommand.js.map +1 -1
- package/dist/commands/prDiscovery.d.ts +24 -25
- package/dist/commands/prDiscovery.js +55 -132
- package/dist/commands/prDiscovery.js.map +1 -1
- package/dist/commands/requirementsDiscovery.d.ts +82 -0
- package/dist/commands/requirementsDiscovery.js +127 -0
- package/dist/commands/requirementsDiscovery.js.map +1 -0
- package/dist/commands/reviewCommand.js +75 -8
- package/dist/commands/reviewCommand.js.map +1 -1
- package/dist/commands/reviewDiscovery.d.ts +96 -0
- package/dist/commands/reviewDiscovery.js +180 -0
- package/dist/commands/reviewDiscovery.js.map +1 -0
- package/dist/tui/approvalBridges.d.ts +86 -0
- package/dist/tui/approvalBridges.js +147 -0
- package/dist/tui/approvalBridges.js.map +1 -0
- package/dist/tui/components/App.js +272 -27
- package/dist/tui/components/App.js.map +1 -1
- package/dist/tui/components/DebugPanel.d.ts +6 -3
- package/dist/tui/components/DebugPanel.js +32 -10
- package/dist/tui/components/DebugPanel.js.map +1 -1
- package/dist/tui/components/FetchProgress.d.ts +1 -1
- package/dist/tui/components/LaunchBanner.d.ts +4 -3
- package/dist/tui/components/LaunchBanner.js +8 -6
- package/dist/tui/components/LaunchBanner.js.map +1 -1
- package/dist/tui/components/LiveTurn.d.ts +15 -9
- package/dist/tui/components/LiveTurn.js +15 -14
- package/dist/tui/components/LiveTurn.js.map +1 -1
- package/dist/tui/components/PromptInput.d.ts +2 -2
- package/dist/tui/components/PromptInput.js +11 -6
- package/dist/tui/components/PromptInput.js.map +1 -1
- package/dist/tui/components/ResumePicker.d.ts +69 -0
- package/dist/tui/components/ResumePicker.js +93 -0
- package/dist/tui/components/ResumePicker.js.map +1 -0
- package/dist/tui/components/SelectList.d.ts +1 -1
- package/dist/tui/components/SelectList.js +1 -1
- package/dist/tui/components/SlashCommandMenu.d.ts +1 -1
- package/dist/tui/components/SlashCommandMenu.js +1 -1
- package/dist/tui/components/StatusBar.d.ts +27 -1
- package/dist/tui/components/StatusBar.js +13 -2
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/debugRender.d.ts +26 -3
- package/dist/tui/debugRender.js +232 -2
- package/dist/tui/debugRender.js.map +1 -1
- package/dist/tui/fixtureAgent.d.ts +2 -2
- package/dist/tui/fixtureAgent.js +2 -2
- package/dist/tui/keyBindings.js +7 -1
- package/dist/tui/keyBindings.js.map +1 -1
- package/dist/tui/keyGuards.d.ts +1 -1
- package/dist/tui/keyGuards.js +1 -1
- package/dist/tui/ruleWidth.d.ts +1 -1
- package/dist/tui/ruleWidth.js +1 -1
- package/dist/tui/selectCancelled.d.ts +1 -1
- package/dist/tui/selectCancelled.js +1 -1
- package/dist/tui/transcriptWindow.js +7 -1
- package/dist/tui/transcriptWindow.js.map +1 -1
- package/dist/tui/tuiSessionModule.js +89 -190
- package/dist/tui/tuiSessionModule.js.map +1 -1
- package/dist/tui/types.d.ts +77 -18
- package/dist/tui/useMouse.d.ts +1 -1
- package/dist/tui/useMouse.js +1 -1
- package/dist/tui/useSlothAnimation.d.ts +1 -1
- package/dist/tui/viewModel.d.ts +64 -12
- package/dist/tui/viewModel.js +43 -11
- package/dist/tui/viewModel.js.map +1 -1
- package/dist/utils/configErrorGuard.d.ts +11 -3
- package/dist/utils/configErrorGuard.js +11 -3
- package/dist/utils/configErrorGuard.js.map +1 -1
- package/package.json +18 -18
|
@@ -5,6 +5,9 @@ import { foldEvents, foldSubagentEvents, initialSubagentTree, initialTurnViewMod
|
|
|
5
5
|
import { buildRejectionMessage } from '@gaunt-sloth/core/core/shell/rejection.js';
|
|
6
6
|
import { ApprovalStopError } from '@gaunt-sloth/core/core/shell/approvalStop.js';
|
|
7
7
|
import { shouldAnnounceTermination } from '@gaunt-sloth/core/core/terminationNotice.js';
|
|
8
|
+
import { runEndReport } from '@gaunt-sloth/core/core/runRecap.js';
|
|
9
|
+
import { providerErrorNotice } from '@gaunt-sloth/core/core/providerErrorNotice.js';
|
|
10
|
+
import { readWaitNarration } from '@gaunt-sloth/core/core/waitNarration.js';
|
|
8
11
|
import { attackBannerCopy, grantsRunAnyway, } from '@gaunt-sloth/core/core/shell/escalationSeverity.js';
|
|
9
12
|
import { TranscriptViewport } from '#src/tui/components/TranscriptViewport.js';
|
|
10
13
|
import { ApprovalPrompt } from '#src/tui/components/ApprovalPrompt.js';
|
|
@@ -22,15 +25,23 @@ import { ClearBanner } from '#src/tui/components/ClearBanner.js';
|
|
|
22
25
|
import { LaunchBanner } from '#src/tui/components/LaunchBanner.js';
|
|
23
26
|
import { MouseProvider } from '#src/tui/useMouse.js';
|
|
24
27
|
import { DebugPanel, debugPanelLines, debugPanelRows, DEBUG_TABS, } from '#src/tui/components/DebugPanel.js';
|
|
28
|
+
import { renderApprovalDetails } from '#src/tui/debugRender.js';
|
|
25
29
|
import { approvalPostureChoices, approvalsRungNotice, approvalsRefusalsNotice, approvalsStatusNotice, approvalsTrustNotice, approvalsUndenyNotice, COMPACTING_LINE, compactionFailedNotice, autocompactNotice, autocompactUnavailableNotice, compactionNotice, compactionUnavailableNotice, createCommandRegistry, dispatchSlashCommand, parseSlashCommand, toolsToggleNotice, } from '@gaunt-sloth/agent/modules/slashCommands.js';
|
|
26
|
-
import { resumedConversationNotice, resumeFailedNotice,
|
|
30
|
+
import { resumableConversationsNotice, resumedConversationNotice, resumeFailedNotice, resumeRefusalNotice, resumeSameConversationNotice, resumeUnavailableNotice, } from '@gaunt-sloth/agent/modules/sessionResume.js';
|
|
27
31
|
import { ApprovalsPicker } from '#src/tui/components/ApprovalsPicker.js';
|
|
32
|
+
import { ResumePicker, resumeCancelledNotice } from '#src/tui/components/ResumePicker.js';
|
|
28
33
|
import { TerminalSizeProvider, useTerminalSize } from '#src/tui/useTerminalSize.js';
|
|
29
34
|
import { findMatches, scrollOffsetForLine, stepMatch } from '#src/tui/debugSearch.js';
|
|
30
35
|
import { useTranscriptScroll } from '#src/tui/useTranscriptScroll.js';
|
|
31
36
|
import { isComposingKeystroke, WHEEL_ROWS_PER_NOTCH } from '#src/tui/transcriptScroll.js';
|
|
32
37
|
import { TUI_HINT_SUFFIX, TUI_KEY_BINDINGS } from '#src/tui/keyBindings.js';
|
|
33
38
|
import { isTypedText, typedText } from '#src/tui/keyGuards.js';
|
|
39
|
+
/**
|
|
40
|
+
* The empty line list a debug tab that is not on screen hands the panel. A module-level constant so
|
|
41
|
+
* its identity is stable across renders: a fresh `[]` each time would make the memo that clamps the
|
|
42
|
+
* scroll offset recompute on every render of every other tab.
|
|
43
|
+
*/
|
|
44
|
+
const NO_DEBUG_LINES = [];
|
|
34
45
|
/** Rows of clipping viewport in the docked debug panel (default / restored size). */
|
|
35
46
|
const DEBUG_VIEWPORT_HEIGHT = 8;
|
|
36
47
|
/** Rows of chrome around the debug viewport we leave for when it is maximised (panel
|
|
@@ -128,6 +139,15 @@ export function App(props) {
|
|
|
128
139
|
const [transcript, setTranscript] = useState(seeded);
|
|
129
140
|
const [live, setLive] = useState(null);
|
|
130
141
|
const [running, setRunning] = useState(false);
|
|
142
|
+
/**
|
|
143
|
+
* [[EXT-92]] scope (a) — what the run is waiting on, when that is not the model answering.
|
|
144
|
+
*
|
|
145
|
+
* Held as the whole rendered sentence rather than an activity code, because the words are
|
|
146
|
+
* `core/waitNarration.ts`'s to choose: it is the module that reaches every surface, and a second
|
|
147
|
+
* spelling of the same wait on this one is the drift the exported prefix exists to prevent. The
|
|
148
|
+
* status bar renders it in place of its own default label, so this costs no row.
|
|
149
|
+
*/
|
|
150
|
+
const [waitActivity, setWaitActivity] = useState(undefined);
|
|
131
151
|
const [turnCount, setTurnCount] = useState(props.resumed?.turns.length ?? 0);
|
|
132
152
|
// Subagent tree, folded from `task` tool calls on the live event stream. No backend dispatches
|
|
133
153
|
// subagents in this release, so nothing fills it today; kept for GS2-25's lean primitive.
|
|
@@ -169,6 +189,11 @@ export function App(props) {
|
|
|
169
189
|
// chrome: unlike the status notice it is never committed to the transcript, and it owns the
|
|
170
190
|
// keyboard while mounted (the prompt is suspended below, as it is for a pending approval).
|
|
171
191
|
const [approvalsPicker, setApprovalsPicker] = useState(null);
|
|
192
|
+
// GS2-112 — the open bare-`/resume` picker's candidates, or null when none is showing. Transient
|
|
193
|
+
// chrome on the same terms as the approvals picker above: never committed to the transcript, and
|
|
194
|
+
// it owns the keyboard while mounted. The list is the snapshot taken when the picker opened; a
|
|
195
|
+
// landed resume replaces the screen, so the picker is closed before the choice is applied.
|
|
196
|
+
const [resumePicker, setResumePicker] = useState(null);
|
|
172
197
|
// Whether to show the post-`/clear` "history cleared" banner. Hidden again the moment the next
|
|
173
198
|
// user turn starts so it doesn't linger above a fresh conversation.
|
|
174
199
|
const [clearedBanner, setClearedBanner] = useState(false);
|
|
@@ -221,6 +246,14 @@ export function App(props) {
|
|
|
221
246
|
// GS2-20 — starts past the seeded items so a later push never reuses a seeded key.
|
|
222
247
|
const idRef = useRef(seeded.length);
|
|
223
248
|
const runningRef = useRef(false);
|
|
249
|
+
/**
|
|
250
|
+
* [[EXT-92]] scope (e) — whether the turn now ending was ended by a throw.
|
|
251
|
+
*
|
|
252
|
+
* A ref rather than state because the only reader is the `finally` of the same async function
|
|
253
|
+
* the catch runs in: a `setState` would not be visible there, and the fact has to be true at the
|
|
254
|
+
* instant the turn is committed, not on the next render.
|
|
255
|
+
*/
|
|
256
|
+
const turnErroredRef = useRef(false);
|
|
224
257
|
/**
|
|
225
258
|
* GS2-23 — whether a `/compact` is awaiting its summary. Read where `runningRef` is read at
|
|
226
259
|
* submit, so a plain message cannot start a turn on a thread that is about to be rewritten, and
|
|
@@ -249,6 +282,22 @@ export function App(props) {
|
|
|
249
282
|
*/
|
|
250
283
|
const conversationIdRef = useRef(props.conversationId);
|
|
251
284
|
const turnCountRef = useRef(props.resumed?.turns.length ?? 0);
|
|
285
|
+
/**
|
|
286
|
+
* [[EXT-178]] — which conversation the outstanding recap request still belongs to.
|
|
287
|
+
*
|
|
288
|
+
* The recap is a model call launched detached at the end of a turn, so up to
|
|
289
|
+
* `RUN_RECAP_TIMEOUT_MS` can pass between asking and answering, and the session does not stand
|
|
290
|
+
* still for it: `/clear` empties the transcript the recap is describing, and a new turn can
|
|
291
|
+
* start and finish inside the window. Appending then puts a summary of a wiped conversation onto
|
|
292
|
+
* a blank screen, or files the previous turn's recap under this one.
|
|
293
|
+
*
|
|
294
|
+
* So the request carries the epoch it was made in and drops itself if the epoch has moved. It is
|
|
295
|
+
* bumped by exactly the three events that make a pending recap wrong: a new turn starting,
|
|
296
|
+
* `/clear`, and this component unmounting. It is deliberately NOT `turnCountRef`, which counts
|
|
297
|
+
* turns for the status bar, is incremented after the recap is launched, and is reset to 0 by
|
|
298
|
+
* `/clear` — a counter that goes backwards cannot be compared for staleness.
|
|
299
|
+
*/
|
|
300
|
+
const recapEpochRef = useRef(0);
|
|
252
301
|
// Per-turn args buffers for the subagent fold (mirrors foldSubagentTree's internal map).
|
|
253
302
|
const subagentBuffersRef = useRef(new Map());
|
|
254
303
|
const debugFocusedRef = useRef(false);
|
|
@@ -260,10 +309,11 @@ export function App(props) {
|
|
|
260
309
|
// inside <PromptInput>, where it lives.
|
|
261
310
|
const promptHandleRef = useRef(null);
|
|
262
311
|
// TUI-C51 — the one thing about the prompt that has to outlive it: the message a command was
|
|
263
|
-
// dispatched beside. The prompt is unmounted in
|
|
264
|
-
// opening
|
|
265
|
-
// here, where nothing unmounts, and the prompt takes it back when
|
|
266
|
-
// rather than state: nothing here renders it, and the prompt empties
|
|
312
|
+
// dispatched beside. The prompt is unmounted in five states below, and two of them (`/approvals`
|
|
313
|
+
// and a bare `/resume` opening their pickers) are entered by a command dispatched FROM the
|
|
314
|
+
// prompt — so the draft is put here, where nothing unmounts, and the prompt takes it back when
|
|
315
|
+
// it returns. It stays a slot rather than state: nothing here renders it, and the prompt empties
|
|
316
|
+
// it as soon as it has it.
|
|
267
317
|
const promptDraftCarryRef = useRef(null);
|
|
268
318
|
// TUI-C79 — set the moment a teardown starts, so a second `quit()` can tell "leaving" from
|
|
269
319
|
// "already leaving" and skip straight to the unmount. See `quit()`.
|
|
@@ -283,6 +333,18 @@ export function App(props) {
|
|
|
283
333
|
const debugViewport = debugViewportHeight(debugMaximized, terminalRows);
|
|
284
334
|
// PageUp/PageDown step tracks the live viewport so maximise pages by (almost) a full screen.
|
|
285
335
|
const debugPageStep = Math.max(1, debugViewport - 1);
|
|
336
|
+
// [[TUI-C27]] — the Auto-mode tab's lines: the approvals gate's own record of every gated tool
|
|
337
|
+
// call, plus the rater config in force (already held here, since `/approvals` moves it).
|
|
338
|
+
//
|
|
339
|
+
// **Read fresh on every render while that tab is on screen, deliberately, and memoised on
|
|
340
|
+
// nothing.** The decision this tab exists for emits no event: a call the rater approves on its
|
|
341
|
+
// first rating relays nothing to any surface — the tool simply runs — so anything keyed on the
|
|
342
|
+
// approval, negotiation or turn signals the App already has would look live and miss exactly it.
|
|
343
|
+
// A pull costs one render of a capped (50-record) buffer, and only while a user is looking at
|
|
344
|
+
// the tab; every other tab keeps the stable empty array and pays nothing.
|
|
345
|
+
const debugAuto = debugVisible && debugTab === 'auto'
|
|
346
|
+
? renderApprovalDetails(props.readApprovalCaptures?.() ?? [], approvals).split('\n')
|
|
347
|
+
: NO_DEBUG_LINES;
|
|
286
348
|
// The active section's line count drives the real maximum scroll offset, so neither the page
|
|
287
349
|
// step nor the arrow step can push the offset past the end (the over-scroll bug that left
|
|
288
350
|
// PgUp/↑ burning through phantom offset before anything moved — TUI-C11). Single-sourced with
|
|
@@ -294,8 +356,9 @@ export function App(props) {
|
|
|
294
356
|
toolsLines: debugTools,
|
|
295
357
|
mcpLines: debugMcp,
|
|
296
358
|
responseLines: debugResponse,
|
|
359
|
+
autoLines: debugAuto,
|
|
297
360
|
activeTab: debugTab,
|
|
298
|
-
}), [subagents, debugHistory, debugSystem, debugTools, debugMcp, debugResponse, debugTab]);
|
|
361
|
+
}), [subagents, debugHistory, debugSystem, debugTools, debugMcp, debugResponse, debugAuto, debugTab]);
|
|
299
362
|
const debugLineCount = debugLines.length;
|
|
300
363
|
const debugMaxOffset = Math.max(0, debugLineCount - debugViewport);
|
|
301
364
|
// Match line-indices for the current query over the active tab's lines — one search, every tab
|
|
@@ -394,6 +457,8 @@ export function App(props) {
|
|
|
394
457
|
// A new exchange supersedes the post-/clear banner; drop it so it doesn't sit above
|
|
395
458
|
// the fresh conversation.
|
|
396
459
|
setClearedBanner(false);
|
|
460
|
+
// [[EXT-178]] — and it supersedes the previous turn's recap, which may still be in flight.
|
|
461
|
+
recapEpochRef.current += 1;
|
|
397
462
|
push({ kind: 'user', text: userInput });
|
|
398
463
|
// [[TUI-C69]] §5.4 — a new user turn is a person being reached, which ENDS any negotiation
|
|
399
464
|
// still standing (the runner clears its own transcript on the same event). Rounds from the
|
|
@@ -403,11 +468,21 @@ export function App(props) {
|
|
|
403
468
|
abortRef.current = ac;
|
|
404
469
|
runningRef.current = true;
|
|
405
470
|
setRunning(true);
|
|
471
|
+
// [[EXT-92]] — both per-turn facts start clean. `turnErroredRef` (scope e) is set by the
|
|
472
|
+
// catch and read by the `finally`; `waitActivity` (scope a) is a label the status bar may
|
|
473
|
+
// still be holding from the previous turn's last wait.
|
|
474
|
+
turnErroredRef.current = false;
|
|
475
|
+
setWaitActivity(undefined);
|
|
406
476
|
setLiveTurn(initialTurnViewModel());
|
|
407
477
|
// Fresh args-buffer map per turn so partial `task` JSON from a prior turn never bleeds in.
|
|
408
478
|
subagentBuffersRef.current = new Map();
|
|
409
479
|
try {
|
|
410
480
|
for await (const event of agent.runTurn(userInput, ac.signal)) {
|
|
481
|
+
// [[EXT-92]] scope (a) — an event arriving IS the wait ending. Clearing here rather than
|
|
482
|
+
// waiting for a paired "done" narration is what keeps the core signal one-way and the
|
|
483
|
+
// plain surface to exactly one line per wait: there is no end message to emit, so there
|
|
484
|
+
// is none to print. The status bar falls back to its own default label.
|
|
485
|
+
setWaitActivity(undefined);
|
|
411
486
|
// [[TUI-C99]] — folded from the REF, not from a local: an approval answered mid-turn
|
|
412
487
|
// writes the outcome onto its tool call from a keystroke, and a local carried across the
|
|
413
488
|
// loop would overwrite it on the very next event the resumed run produces.
|
|
@@ -427,16 +502,43 @@ export function App(props) {
|
|
|
427
502
|
push({ kind: 'stop', parts: err.parts });
|
|
428
503
|
}
|
|
429
504
|
else {
|
|
505
|
+
// [[EXT-92]] scope (b) — a provider error is rendered as prose here, and the raw payload
|
|
506
|
+
// rides along in a field this item's renderer never prints.
|
|
507
|
+
//
|
|
508
|
+
// The whole defect was on this line. `err.message` for an OpenRouter refusal is
|
|
509
|
+
// `Provider returned error | metadata: {…}` — upstream flattens its own structured
|
|
510
|
+
// metadata into the message — so the user read a JSON blob while the provider's own
|
|
511
|
+
// sentence, its remedy hint, and the fact that it was a SHARED pool rather than their
|
|
512
|
+
// quota all sat unread inside it. Nothing had been lost: the same fields are still
|
|
513
|
+
// objects on `err`, which is why this needed a renderer rather than a parser.
|
|
514
|
+
// `providerErrorNotice` returns null for the ordinary runtime errors it has nothing to
|
|
515
|
+
// add to, and those keep exactly the rendering they had.
|
|
516
|
+
const notice = providerErrorNotice(err);
|
|
430
517
|
push({
|
|
431
518
|
kind: 'system',
|
|
432
519
|
level: 'error',
|
|
433
|
-
text: err instanceof Error ? err.message : String(err),
|
|
520
|
+
text: notice?.text ?? (err instanceof Error ? err.message : String(err)),
|
|
521
|
+
...(notice ? { raw: notice.raw } : {}),
|
|
434
522
|
});
|
|
435
523
|
}
|
|
524
|
+
// [[EXT-92]] scope (e) — remember that this turn was ended by the failure above, so the
|
|
525
|
+
// `finally` can commit it as a turn that STOPPED rather than one that finished. An
|
|
526
|
+
// approvals stop is deliberately excluded: it already commits its own `stop` item saying
|
|
527
|
+
// so in full, and marking the turn as well would state the same ending twice.
|
|
528
|
+
if (!(err instanceof ApprovalStopError))
|
|
529
|
+
turnErroredRef.current = true;
|
|
436
530
|
}
|
|
437
531
|
finally {
|
|
532
|
+
// [[EXT-92]] scope (a) — the turn is over, so nothing is being waited on. In the `finally`
|
|
533
|
+
// because the endings that most need it are the ones that reach neither the loop's normal
|
|
534
|
+
// exit nor the catch: a turn cancelled mid-rating would otherwise leave the bar naming a
|
|
535
|
+
// wait that no longer exists.
|
|
536
|
+
setWaitActivity(undefined);
|
|
438
537
|
const vm = liveVmRef.current ?? initialTurnViewModel();
|
|
439
|
-
push({
|
|
538
|
+
push({
|
|
539
|
+
kind: 'assistant',
|
|
540
|
+
turn: turnErroredRef.current ? { ...vm, endedInError: true } : vm,
|
|
541
|
+
});
|
|
440
542
|
// [[EXT-159]] — and, immediately under the answer, why the turn ended when that is not
|
|
441
543
|
// simply "the model finished".
|
|
442
544
|
//
|
|
@@ -454,6 +556,50 @@ export function App(props) {
|
|
|
454
556
|
const reason = agent.getTerminationReason?.() ?? null;
|
|
455
557
|
if (reason && shouldAnnounceTermination(reason))
|
|
456
558
|
push({ kind: 'termination', reason });
|
|
559
|
+
// [[EXT-158]] — and the other silence: a turn that ended cleanly with its own checklist
|
|
560
|
+
// still carrying work. `shouldAnnounceTermination` declines `completed`, correctly — an
|
|
561
|
+
// ordinary answer needs no epitaph — which is exactly why this case had nothing at all.
|
|
562
|
+
//
|
|
563
|
+
// Committed as an ordinary `notice` item rather than a transcript kind of its own. The
|
|
564
|
+
// termination item earns its kind by carrying the REASON value so the transcript holds
|
|
565
|
+
// the classification rather than a rendered line; this fact has no such closed taxonomy
|
|
566
|
+
// to preserve, and a new kind would buy a second row-count case and a second renderer for
|
|
567
|
+
// a title and three lines the generic notice already draws.
|
|
568
|
+
//
|
|
569
|
+
// [[EXT-178]] — and its successor when the user has switched the recap on. The two speak
|
|
570
|
+
// for the same population, so exactly one of them is drawn: `runEndReport` decides, with
|
|
571
|
+
// the recap already resolved, and a recap that never arrived leaves the notice standing.
|
|
572
|
+
//
|
|
573
|
+
// **Detached from the turn's teardown, and that is the whole reason it is down here.**
|
|
574
|
+
// A recap is a model call made after the answer is already on screen; awaiting it in this
|
|
575
|
+
// `finally` would keep the spinner running and the prompt away for as long as a provider
|
|
576
|
+
// takes to reply, which is a worse thing to do to a finished turn than a late paragraph.
|
|
577
|
+
// The transcript is append-only and this lands at the bottom, where the reader is. With
|
|
578
|
+
// the default `off` rung nothing is contacted and the notice lands as it always did.
|
|
579
|
+
const work = agent.getOutstandingWork?.() ?? null;
|
|
580
|
+
const ending = reason;
|
|
581
|
+
// Detached means the session can move on underneath it, so the request carries the epoch
|
|
582
|
+
// it was made in and says nothing if `recapEpochRef` has moved on — see that ref.
|
|
583
|
+
const epoch = recapEpochRef.current;
|
|
584
|
+
void (async () => {
|
|
585
|
+
try {
|
|
586
|
+
const recap = (await agent.requestRunRecap?.(ending)) ?? null;
|
|
587
|
+
if (recapEpochRef.current !== epoch)
|
|
588
|
+
return;
|
|
589
|
+
const report = runEndReport(recap, work, ending);
|
|
590
|
+
if (report.kind !== 'silent') {
|
|
591
|
+
push({
|
|
592
|
+
kind: 'notice',
|
|
593
|
+
title: report.notice.title,
|
|
594
|
+
lines: report.notice.lines,
|
|
595
|
+
tone: report.kind === 'recap' ? 'info' : 'warn',
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
/* fail-soft: explaining a turn must never be what breaks the session */
|
|
601
|
+
}
|
|
602
|
+
})();
|
|
457
603
|
}
|
|
458
604
|
catch {
|
|
459
605
|
/* fail-soft: explaining a turn must never be what breaks the session */
|
|
@@ -715,7 +861,31 @@ export function App(props) {
|
|
|
715
861
|
// replaces the screen: the transcript on it belonged to the conversation being left, and the
|
|
716
862
|
// model is now continuing another one, so the banner and the restored turns take its place —
|
|
717
863
|
// the same wipe `/clear` does, without the cleared banner, because nothing was discarded (the
|
|
718
|
-
// conversation left keeps everything recorded under it). Bare,
|
|
864
|
+
// conversation left keeps everything recorded under it). Bare, this surface offers the picker
|
|
865
|
+
// below; the readline session prints the list.
|
|
866
|
+
//
|
|
867
|
+
// GS2-112 — **why the interactive picker is one branch here and not a new protocol field.**
|
|
868
|
+
//
|
|
869
|
+
// `/resume` returns `resume?: { id?: number }`, and the missing id is not an omission to be
|
|
870
|
+
// filled in later: the command is pure and cannot reach the store or the runner, so the no-id
|
|
871
|
+
// case has always meant *the surface fulfils this*, which is what every other surface already
|
|
872
|
+
// does with it. The Ink TUI can show a list and read arrow keys; the readline session can print
|
|
873
|
+
// one; a server surface has no user at a keyboard at all. Choosing is therefore the surface's
|
|
874
|
+
// answer to the same request, and nothing in the request needs to change for this surface to
|
|
875
|
+
// give a better one.
|
|
876
|
+
//
|
|
877
|
+
// The alternative was weighed and rejected rather than overlooked. A cross-package chooser — a
|
|
878
|
+
// new result field carrying candidates, or a `chooseConversation` callback threaded from the
|
|
879
|
+
// agent down into the component — would put a widget's contract in the module both surfaces
|
|
880
|
+
// read from, to serve one surface, and would still leave this branch deciding when to open it.
|
|
881
|
+
// It buys nothing: the candidates are already here (`props.listResumeCandidates`, wired in
|
|
882
|
+
// `tuiSessionModule`), and the id the user picks re-enters the with-id path below, which is the
|
|
883
|
+
// same code `--resume <id>` runs at boot. One branch, one new component, no new protocol.
|
|
884
|
+
//
|
|
885
|
+
// **The readline surface keeps the printed list deliberately.** It is not a fallback that has
|
|
886
|
+
// been left behind: with no Ink tree there is nothing to arrow through, and the printed ids
|
|
887
|
+
// remain the only way in on that surface — so `resumableConversationsNotice` stays its list
|
|
888
|
+
// builder, and no picker wording is allowed to leak into it (pinned by a spec).
|
|
719
889
|
const applyResume = useCallback(async (id) => {
|
|
720
890
|
const commit = (notice) => push({
|
|
721
891
|
kind: 'notice',
|
|
@@ -724,11 +894,23 @@ export function App(props) {
|
|
|
724
894
|
tone: notice.tone ?? 'info',
|
|
725
895
|
});
|
|
726
896
|
if (id === undefined) {
|
|
897
|
+
// A session with no store behind it cannot list anything, let alone offer it: this prop is
|
|
898
|
+
// wired only where history is on and a store opened, so its absence is the same "resume is
|
|
899
|
+
// unavailable here" the with-id path reports below.
|
|
727
900
|
if (!props.listResumeCandidates) {
|
|
728
901
|
commit(resumeUnavailableNotice());
|
|
729
902
|
return;
|
|
730
903
|
}
|
|
731
|
-
|
|
904
|
+
const candidates = props.listResumeCandidates();
|
|
905
|
+
// With nothing to offer, say so and open nothing. An empty <SelectList> renders a "no
|
|
906
|
+
// matches" line and its Enter is inert, so a picker here would be a modal the user can
|
|
907
|
+
// only escape from — strictly worse than the sentence, which also explains what makes a
|
|
908
|
+
// conversation resumable.
|
|
909
|
+
if (candidates.length === 0) {
|
|
910
|
+
commit(resumableConversationsNotice(candidates));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
setResumePicker(candidates);
|
|
732
914
|
return;
|
|
733
915
|
}
|
|
734
916
|
if (!agent.resumeConversation) {
|
|
@@ -1049,6 +1231,10 @@ export function App(props) {
|
|
|
1049
1231
|
conversationId: conversationIdRef.current,
|
|
1050
1232
|
toolsExpanded: toolsExpandedRef.current,
|
|
1051
1233
|
debugVisible: debugVisibleRef.current,
|
|
1234
|
+
// TUI-C96 — this surface has the slash-command menu (<PromptInput> renders it
|
|
1235
|
+
// unconditionally), so the unknown-command notice may explain a shortened name by
|
|
1236
|
+
// pointing at it. The readline surface says false and gets the other sentence.
|
|
1237
|
+
hasSlashMenu: true,
|
|
1052
1238
|
// TUI-C37 — undefined (not false) when this surface has no mouse layer, so `/mouse`
|
|
1053
1239
|
// says it is unavailable rather than silently reporting "off".
|
|
1054
1240
|
mouseEnabled: props.mouseEnabled === undefined ? undefined : mouseEnabledRef.current,
|
|
@@ -1059,6 +1245,10 @@ export function App(props) {
|
|
|
1059
1245
|
// TUI-C19 — the actual validation warnings so `/config` renders the details the
|
|
1060
1246
|
// standing advisory line points at.
|
|
1061
1247
|
configWarnings: props.advisories,
|
|
1248
|
+
// GS2-88 — the three summaries AND the reason they may be empty, so a command with
|
|
1249
|
+
// nothing to show names the cause the session actually established rather than the
|
|
1250
|
+
// config. Both surfaces pass all four, from the one core builder.
|
|
1251
|
+
historyAvailability: props.historyAvailability,
|
|
1062
1252
|
historySummary: props.historySummary,
|
|
1063
1253
|
insightsSummary: props.insightsSummary,
|
|
1064
1254
|
historySearch: props.historySearch,
|
|
@@ -1097,11 +1287,19 @@ export function App(props) {
|
|
|
1097
1287
|
// thread intact (the LangGraph checkpointer replays it on the next turn), so the
|
|
1098
1288
|
// model would still "remember" everything. Reset the agent's thread too so the
|
|
1099
1289
|
// model context truly matches the now-empty transcript (TUI-C8).
|
|
1100
|
-
|
|
1290
|
+
//
|
|
1291
|
+
// [[EXT-109]] — and the agent's own record of the conversation goes with it: the
|
|
1292
|
+
// approvals gate's capture log is what the Auto-mode tab above and a later
|
|
1293
|
+
// `/debug-dump` read, and every other pane cleared here would otherwise sit beside one
|
|
1294
|
+
// that still quotes the user's pre-`/clear` messages.
|
|
1295
|
+
agent.clearConversation?.();
|
|
1101
1296
|
// The status-bar turn counter is part of the conversation state we just wiped, so
|
|
1102
1297
|
// reset it too — a cleared session starts back at "turns: 0".
|
|
1103
1298
|
turnCountRef.current = 0;
|
|
1104
1299
|
setTurnCount(0);
|
|
1300
|
+
// [[EXT-178]] — a recap still in flight describes the conversation just wiped, so drop
|
|
1301
|
+
// it rather than append a summary of nothing onto a cleared screen.
|
|
1302
|
+
recapEpochRef.current += 1;
|
|
1105
1303
|
}
|
|
1106
1304
|
if (result.toggleTools) {
|
|
1107
1305
|
// Flip the fold mode and commit the notice via the shared helper (single-sourced with
|
|
@@ -1206,7 +1404,7 @@ export function App(props) {
|
|
|
1206
1404
|
// path never registers gsloth's readline Esc handler, so Ink owns raw mode cleanly).
|
|
1207
1405
|
// 3. Tab while the panel is visible and idle → focus/cycle the panel.
|
|
1208
1406
|
// 4. While the panel is focused → Tab/Shift+Tab cycle sections, ↑/↓ scroll one line,
|
|
1209
|
-
// PageUp/PageDown page-step, m maximises, Esc
|
|
1407
|
+
// PageUp/PageDown page-step, m maximises, Esc leaves the pane (restoring a maximised one).
|
|
1210
1408
|
useInput((input, key) => {
|
|
1211
1409
|
// [[TUI-C79]] — **Ctrl+C is this app's, not Ink's**, and it is answered here before anything
|
|
1212
1410
|
// else can claim it. `render()` takes `exitOnCtrlC: false` (`tuiSessionModule.tsx`), which is
|
|
@@ -1227,13 +1425,19 @@ export function App(props) {
|
|
|
1227
1425
|
// bare `exit` keyword all take it too).
|
|
1228
1426
|
//
|
|
1229
1427
|
// **The modal states are answered first, and they exit.** While an attack banner, an approval
|
|
1230
|
-
// prompt or the approvals
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1428
|
+
// prompt or one of the pickers (`/approvals`, bare `/resume`) owns the keyboard, the prompt is
|
|
1429
|
+
// unmounted, so rung 1 has no buffer to scrap; and where that modal state was reached during a
|
|
1430
|
+
// turn, rung 2 would read "stop the turn" while the banner's own controls line says `Ctrl+C
|
|
1431
|
+
// exits gth` and the user is looking for the way out of an irreversible action. So it exits, as
|
|
1432
|
+
// the screen promises — and it goes through `quit()`, which resolves both bridges fail-closed
|
|
1433
|
+
// on the way out rather than leaving the halt unanswered.
|
|
1434
|
+
//
|
|
1435
|
+
// GS2-112 — the resume picker is in this set for coherence rather than for a case it changes
|
|
1436
|
+
// today: `/resume` is idle-only and the prompt is unmounted while its picker is up, so no turn
|
|
1437
|
+
// can be started under it and the ladder would fall to rung 3 and exit anyway. Naming it here
|
|
1438
|
+
// is what keeps that answer from depending on which rung happens to catch it.
|
|
1235
1439
|
if (key.ctrl && input === 'c') {
|
|
1236
|
-
if (pendingAttack || pendingApproval || approvalsPicker) {
|
|
1440
|
+
if (pendingAttack || pendingApproval || approvalsPicker || resumePicker) {
|
|
1237
1441
|
quit();
|
|
1238
1442
|
return;
|
|
1239
1443
|
}
|
|
@@ -1369,11 +1573,11 @@ export function App(props) {
|
|
|
1369
1573
|
resolveApproval('reject');
|
|
1370
1574
|
return;
|
|
1371
1575
|
}
|
|
1372
|
-
// CFG-39 — while
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
if (approvalsPicker)
|
|
1576
|
+
// CFG-39, GS2-112 — while a picker is open it OWNS the keyboard, exactly as a pending approval
|
|
1577
|
+
// does. <SelectList> is a useInput subscriber too and Ink runs every subscriber, so without
|
|
1578
|
+
// this the arrow keys would also scroll the transcript and Esc would both cancel the picker and
|
|
1579
|
+
// jump to the newest output. Returning here leaves the picker the only claimant.
|
|
1580
|
+
if (approvalsPicker || resumePicker)
|
|
1377
1581
|
return;
|
|
1378
1582
|
if (key.escape && runningRef.current) {
|
|
1379
1583
|
abortRef.current?.abort();
|
|
@@ -1437,7 +1641,17 @@ export function App(props) {
|
|
|
1437
1641
|
}
|
|
1438
1642
|
return;
|
|
1439
1643
|
}
|
|
1440
|
-
// Esc layering: clear an active search first (less-style), else
|
|
1644
|
+
// Esc layering: clear an active search first (less-style), else LEAVE the pane — and leaving
|
|
1645
|
+
// it drops the maximised view as well as focus, in the one press.
|
|
1646
|
+
//
|
|
1647
|
+
// TUI-C107 — focus and maximisation are one state as far as an exit is concerned, so an exit
|
|
1648
|
+
// that dropped only one of them left the user with nothing to press. The prompt is unmounted
|
|
1649
|
+
// while the pane holds focus (the render's `!debugFocused` guard), and a maximised pane takes
|
|
1650
|
+
// the terminal height minus DEBUG_MAX_CHROME_ROWS, so the prompt has no rows to draw in even
|
|
1651
|
+
// once focus is gone: dropping focus alone left the session with no visible prompt, and `m` —
|
|
1652
|
+
// the key that would undo the maximisation — is handled inside this focused-only branch, so
|
|
1653
|
+
// it no longer answered either. Same pairing, and the same reason, as the /debug toggle's
|
|
1654
|
+
// "focus never lingers on a hidden panel" above.
|
|
1441
1655
|
if (key.escape) {
|
|
1442
1656
|
if (debugSearchQueryRef.current) {
|
|
1443
1657
|
clearDebugSearch();
|
|
@@ -1445,6 +1659,7 @@ export function App(props) {
|
|
|
1445
1659
|
}
|
|
1446
1660
|
setDebugFocused(false);
|
|
1447
1661
|
debugFocusedRef.current = false;
|
|
1662
|
+
setDebugMaximized(false);
|
|
1448
1663
|
return;
|
|
1449
1664
|
}
|
|
1450
1665
|
// `/` opens the search input (scoped to pane focus — the seam).
|
|
@@ -1588,6 +1803,12 @@ export function App(props) {
|
|
|
1588
1803
|
};
|
|
1589
1804
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1590
1805
|
}, []);
|
|
1806
|
+
// [[EXT-178]] — the third thing that invalidates a recap still in flight: this component going
|
|
1807
|
+
// away. A quit unmounts while a provider may still be holding the request open, and the reply
|
|
1808
|
+
// would then land on a session the user has left.
|
|
1809
|
+
useEffect(() => () => {
|
|
1810
|
+
recapEpochRef.current += 1;
|
|
1811
|
+
}, []);
|
|
1591
1812
|
// Route agent status updates (warnings/errors) into the transcript instead of stdout,
|
|
1592
1813
|
// which would otherwise corrupt Ink's frame. INFO/DEBUG system lines are suppressed in the
|
|
1593
1814
|
// TUI: the agent's per-turn chatter (`Requested tools`, `Loaded tools`, `Workdir`, `Model`,
|
|
@@ -1598,6 +1819,17 @@ export function App(props) {
|
|
|
1598
1819
|
if (!props.subscribeStatus)
|
|
1599
1820
|
return;
|
|
1600
1821
|
return props.subscribeStatus((level, message) => {
|
|
1822
|
+
// [[EXT-92]] scope (a) — a wait narration is INFO, and INFO is exactly what this handler
|
|
1823
|
+
// drops, so it is picked out BEFORE the filter and routed to the status bar instead of the
|
|
1824
|
+
// transcript. That is the right destination on both counts: it is a transient fact about the
|
|
1825
|
+
// present moment, which the transcript is the wrong shape for, and the bar is already the
|
|
1826
|
+
// TUI's wait renderer. Recognised by the prefix `core/waitNarration.ts` exports for this,
|
|
1827
|
+
// rather than by a copy of the wording here.
|
|
1828
|
+
const narration = readWaitNarration(message);
|
|
1829
|
+
if (narration !== null) {
|
|
1830
|
+
setWaitActivity(narration);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1601
1833
|
if (level === 'INFO' || level === 'DEBUG')
|
|
1602
1834
|
return;
|
|
1603
1835
|
if (message.trim())
|
|
@@ -1679,10 +1911,19 @@ export function App(props) {
|
|
|
1679
1911
|
: 0) -
|
|
1680
1912
|
(activeChecklist ? checklistPanelRows(activeChecklist) : 0) -
|
|
1681
1913
|
negotiationPanelRows(negotiationRounds, terminalColumns);
|
|
1682
|
-
return (_jsx(TerminalSizeProvider, { size: terminalSize, children: _jsx(MouseProvider, { subscribe: props.subscribeMouse, enabled: mouseEnabled, children: _jsxs(Box, { flexDirection: "column", height: terminalRows, children: [_jsxs(TranscriptViewport, { items: transcript, budgetRows: terminalRows, columns: terminalColumns, toolsExpanded: toolsExpanded, scroll: transcriptScroll, regionRows: transcriptRegionRows, geometry: transcriptGeometry, children: [clearedBanner ? _jsx(ClearBanner, {}) : null, showIntro && props.showLaunchBanner ? (_jsx(LaunchBanner, { model: modelDisplayName, provider: props.modelProviderType })) : null, showIntro ? _jsx(Text, { dimColor: true, children: readyMessage.trim() }) : null, live && transcript.length > 0 ? _jsx(BlankRow, {}) : null, live ? (_jsx(LiveTurn, { turn: live, toolsExpanded: toolsExpanded, streaming: true, columns: terminalColumns })) : null, pendingApproval ? (_jsxs(_Fragment, { children: [live || transcript.length > 0 ? _jsx(BlankRow, {}) : null, _jsx(ApprovalRequestPanel, { pending: pendingApproval.pending, columns: terminalColumns })] })) : null] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [debugVisible ? (_jsx(DebugPanel, { subagents: subagents, historyLines: debugHistory, systemLines: debugSystem, toolsLines: debugTools, mcpLines: debugMcp, responseLines: debugResponse, activeTab: debugTab, scrollOffset: debugScroll, focused: debugFocused, viewportHeight: debugViewport, maximized: debugMaximized, searchQuery: debugSearchQuery, searchActive: debugSearchInput, matchCount: debugMatches.length, currentMatchIndex: debugSearchCurrent, currentMatchLine: currentMatchLine })) : null, activeChecklist ? _jsx(ChecklistPanel, { items: activeChecklist }) : null, _jsx(NegotiationPanel, { rounds: negotiationRounds }), pendingAttack ? _jsx(AttackBanner, { halt: pendingAttack.halt, typed: attackTyped }) : null, pendingApproval && !pendingAttack ? (_jsx(ApprovalPrompt, { pending: pendingApproval.pending })) : null, approvalsPicker && !pendingApproval && !pendingAttack ? (_jsx(ApprovalsPicker, { choices: approvalsPicker, onSelect: (rung) => {
|
|
1914
|
+
return (_jsx(TerminalSizeProvider, { size: terminalSize, children: _jsx(MouseProvider, { subscribe: props.subscribeMouse, enabled: mouseEnabled, children: _jsxs(Box, { flexDirection: "column", height: terminalRows, children: [_jsxs(TranscriptViewport, { items: transcript, budgetRows: terminalRows, columns: terminalColumns, toolsExpanded: toolsExpanded, scroll: transcriptScroll, regionRows: transcriptRegionRows, geometry: transcriptGeometry, children: [clearedBanner ? _jsx(ClearBanner, {}) : null, showIntro && props.showLaunchBanner ? (_jsx(LaunchBanner, { model: modelDisplayName, provider: props.modelProviderType })) : null, showIntro ? _jsx(Text, { dimColor: true, children: readyMessage.trim() }) : null, live && transcript.length > 0 ? _jsx(BlankRow, {}) : null, live ? (_jsx(LiveTurn, { turn: live, toolsExpanded: toolsExpanded, streaming: true, columns: terminalColumns })) : null, pendingApproval ? (_jsxs(_Fragment, { children: [live || transcript.length > 0 ? _jsx(BlankRow, {}) : null, _jsx(ApprovalRequestPanel, { pending: pendingApproval.pending, columns: terminalColumns })] })) : null] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [debugVisible ? (_jsx(DebugPanel, { subagents: subagents, historyLines: debugHistory, systemLines: debugSystem, toolsLines: debugTools, mcpLines: debugMcp, responseLines: debugResponse, autoLines: debugAuto, activeTab: debugTab, scrollOffset: debugScroll, focused: debugFocused, viewportHeight: debugViewport, maximized: debugMaximized, searchQuery: debugSearchQuery, searchActive: debugSearchInput, matchCount: debugMatches.length, currentMatchIndex: debugSearchCurrent, currentMatchLine: currentMatchLine })) : null, activeChecklist ? _jsx(ChecklistPanel, { items: activeChecklist }) : null, _jsx(NegotiationPanel, { rounds: negotiationRounds }), pendingAttack ? _jsx(AttackBanner, { halt: pendingAttack.halt, typed: attackTyped }) : null, pendingApproval && !pendingAttack ? (_jsx(ApprovalPrompt, { pending: pendingApproval.pending })) : null, approvalsPicker && !pendingApproval && !pendingAttack ? (_jsx(ApprovalsPicker, { choices: approvalsPicker, onSelect: (rung) => {
|
|
1683
1915
|
setApprovalsPicker(null);
|
|
1684
1916
|
applyApprovalRung(rung);
|
|
1685
|
-
}, onCancel: () => setApprovalsPicker(null) })) : null,
|
|
1917
|
+
}, onCancel: () => setApprovalsPicker(null) })) : null, resumePicker && !approvalsPicker && !pendingApproval && !pendingAttack ? (_jsx(ResumePicker, { candidates: resumePicker, onSelect: (id) => {
|
|
1918
|
+
// Close BEFORE applying: a landed resume replaces the transcript and re-keys its
|
|
1919
|
+
// ids, and this list is a snapshot of the conversations as they were before it.
|
|
1920
|
+
setResumePicker(null);
|
|
1921
|
+
void applyResume(id);
|
|
1922
|
+
}, onCancel: () => {
|
|
1923
|
+
setResumePicker(null);
|
|
1924
|
+
const notice = resumeCancelledNotice();
|
|
1925
|
+
push({ kind: 'notice', title: notice.title, lines: notice.lines, tone: 'info' });
|
|
1926
|
+
} })) : null, _jsx(BlankRow, {}), _jsx(Rule, {}), _jsx(NoticeBar, { advisories: props.advisories }), _jsx(McpFailureBar, { failures: props.mcpFailures }), _jsx(StatusBar, { running: running, activity: waitActivity, mode: mode, modelDisplayName: modelDisplayName,
|
|
1686
1927
|
// CFG-38 — the provider beside the model, and the live width it has to fit in: the
|
|
1687
1928
|
// bar drops the provider rather than overflowing a narrow terminal.
|
|
1688
1929
|
modelProviderType: props.modelProviderType, columns: terminalColumns, turnCount: turnCount, debugHint: debugVisible && !debugFocused, approvals: approvals
|
|
@@ -1690,7 +1931,11 @@ export function App(props) {
|
|
|
1690
1931
|
rung: approvals.rung,
|
|
1691
1932
|
raterProfile: approvals.rater,
|
|
1692
1933
|
}
|
|
1693
|
-
: undefined }), !debugFocused &&
|
|
1934
|
+
: undefined }), !debugFocused &&
|
|
1935
|
+
!pendingApproval &&
|
|
1936
|
+
!pendingAttack &&
|
|
1937
|
+
!approvalsPicker &&
|
|
1938
|
+
!resumePicker ? (_jsxs(_Fragment, { children: [_jsx(BlankRow, {}), _jsx(PromptInput, { onSubmit: handleSubmit, commands: registry, onMenuStateChange: (active) => {
|
|
1694
1939
|
slashMenuActiveRef.current = active;
|
|
1695
1940
|
}, handleRef: promptHandleRef, draftCarryRef: promptDraftCarryRef, promptBlockRows: promptBlockRows }), _jsx(BlankRow, {})] })) : null, _jsx(Text, { dimColor: true, wrap: "truncate-end", children: `${exitMessage.trim()}${TUI_HINT_SUFFIX}` }), _jsx(Rule, {})] })] }) }) }));
|
|
1696
1941
|
}
|