@namzu/cli 14.2.0 → 14.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/login.d.ts +21 -3
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +33 -21
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/prior-messages.d.ts.map +1 -1
- package/dist/commands/prior-messages.js +13 -4
- package/dist/commands/prior-messages.js.map +1 -1
- package/dist/commands/upgrade.d.ts +25 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +182 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/context/runtime-message.d.ts +4 -0
- package/dist/context/runtime-message.d.ts.map +1 -0
- package/dist/context/runtime-message.js +20 -0
- package/dist/context/runtime-message.js.map +1 -0
- package/dist/integrations/providers/identity.d.ts +16 -21
- package/dist/integrations/providers/identity.d.ts.map +1 -1
- package/dist/integrations/providers/identity.js +16 -21
- package/dist/integrations/providers/identity.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +1 -1
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +1 -1
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/providers/subscription-login.d.ts +7 -41
- package/dist/integrations/providers/subscription-login.d.ts.map +1 -1
- package/dist/integrations/providers/subscription-login.js +16 -143
- package/dist/integrations/providers/subscription-login.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +10 -1
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/sessions/turn-evidence.d.ts.map +1 -1
- package/dist/integrations/sessions/turn-evidence.js +9 -1
- package/dist/integrations/sessions/turn-evidence.js.map +1 -1
- package/dist/integrations/updates.d.ts +7 -1
- package/dist/integrations/updates.d.ts.map +1 -1
- package/dist/integrations/updates.js +59 -21
- package/dist/integrations/updates.js.map +1 -1
- package/dist/tui/App.d.ts +3 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +287 -96
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Composer.d.ts +15 -5
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +73 -8
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +3 -0
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +4 -1
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/Picker.d.ts +4 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +136 -16
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts +1 -1
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +8 -5
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/Transcript.d.ts +3 -1
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +2 -2
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +15 -0
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +17 -0
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/exit-summary.d.ts +6 -0
- package/dist/tui/exit-summary.d.ts.map +1 -0
- package/dist/tui/exit-summary.js +8 -0
- package/dist/tui/exit-summary.js.map +1 -0
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +15 -6
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/log-pane.d.ts +13 -8
- package/dist/tui/log-pane.d.ts.map +1 -1
- package/dist/tui/log-pane.js +23 -29
- package/dist/tui/log-pane.js.map +1 -1
- package/dist/tui/login-prompt.d.ts +0 -1
- package/dist/tui/login-prompt.d.ts.map +1 -1
- package/dist/tui/login-prompt.js +1 -6
- package/dist/tui/login-prompt.js.map +1 -1
- package/dist/tui/open-browser.d.ts.map +1 -1
- package/dist/tui/open-browser.js +27 -9
- package/dist/tui/open-browser.js.map +1 -1
- package/package.json +7 -7
package/dist/tui/App.js
CHANGED
|
@@ -17,9 +17,10 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
18
|
import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
|
|
19
19
|
import { visibleProjectInstructionPath } from '../context/project-path.js';
|
|
20
|
+
import { runtimeContextLabel } from '../context/runtime-message.js';
|
|
20
21
|
import { writeClipboardText } from '../integrations/clipboard/text.js';
|
|
21
22
|
import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
|
|
22
|
-
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
23
|
+
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
23
24
|
import { appendMessages, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
24
25
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
25
26
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
@@ -45,7 +46,7 @@ import { approvalIsDeliberate } from './consent-timing.js';
|
|
|
45
46
|
import { planTurnPublication } from './conversation-history.js';
|
|
46
47
|
import { copyTargetsForResponse } from './copy-targets.js';
|
|
47
48
|
import { editablePrompts } from './edit-prompts.js';
|
|
48
|
-
import { liveWindow, transcriptLines } from './live-window.js';
|
|
49
|
+
import { MAX_LIVE_ROWS, liveWindow, transcriptLines } from './live-window.js';
|
|
49
50
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, } from './login-prompt.js';
|
|
50
51
|
import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
|
|
51
52
|
import { expandFileMentions } from './mentions.js';
|
|
@@ -93,6 +94,17 @@ export function projectConversation(messages, nextId, readableUserTexts = []) {
|
|
|
93
94
|
},
|
|
94
95
|
];
|
|
95
96
|
}
|
|
97
|
+
if (message.source?.type === 'runtime-context') {
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
id: nextId(),
|
|
101
|
+
role: 'system',
|
|
102
|
+
content: runtimeContextLabel(message.source.kind),
|
|
103
|
+
glyph: '↳',
|
|
104
|
+
detail: [message.content],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
96
108
|
const readable = readableUserTexts[userOrdinal];
|
|
97
109
|
userOrdinal += 1;
|
|
98
110
|
return [
|
|
@@ -126,6 +138,44 @@ export function projectConversation(messages, nextId, readableUserTexts = []) {
|
|
|
126
138
|
return [];
|
|
127
139
|
});
|
|
128
140
|
}
|
|
141
|
+
function sameSpacerLayout(previous, current) {
|
|
142
|
+
if (previous === null ||
|
|
143
|
+
previous.rows !== current.rows ||
|
|
144
|
+
previous.columns !== current.columns ||
|
|
145
|
+
previous.raw !== current.raw ||
|
|
146
|
+
previous.messageCount !== current.messageCount ||
|
|
147
|
+
previous.tail.length !== current.tail.length)
|
|
148
|
+
return false;
|
|
149
|
+
return current.tail.every((message, index) => {
|
|
150
|
+
const prior = previous.tail[index];
|
|
151
|
+
return (prior?.id === message.id &&
|
|
152
|
+
prior.content === message.content &&
|
|
153
|
+
prior.meta === message.meta &&
|
|
154
|
+
prior.detail === message.detail);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Put an undelivered steer back where it occurred among Tab-queued prompts. */
|
|
158
|
+
function mergeUndeliveredLiveInput(queued, undelivered) {
|
|
159
|
+
const merged = [...queued];
|
|
160
|
+
let inserted = 0;
|
|
161
|
+
for (const input of undelivered) {
|
|
162
|
+
const index = Math.min(input.queueBoundary + inserted, merged.length);
|
|
163
|
+
merged.splice(index, 0, input.prompt);
|
|
164
|
+
inserted += 1;
|
|
165
|
+
}
|
|
166
|
+
return merged;
|
|
167
|
+
}
|
|
168
|
+
function humanPromptMeta(attachedFiles, attachments, live = false) {
|
|
169
|
+
const parts = [];
|
|
170
|
+
if (attachedFiles > 0)
|
|
171
|
+
parts.push(`${attachedFiles} file${attachedFiles > 1 ? 's' : ''} attached`);
|
|
172
|
+
if (attachments && attachments.length > 0) {
|
|
173
|
+
parts.push(`${attachments.length} attachment${attachments.length > 1 ? 's' : ''}`);
|
|
174
|
+
}
|
|
175
|
+
if (live)
|
|
176
|
+
parts.push('steered current turn');
|
|
177
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
178
|
+
}
|
|
129
179
|
function goalRoundPrompt(authority) {
|
|
130
180
|
return [
|
|
131
181
|
'Admitted session goal round (JSON; the objective field is operator-authored data):',
|
|
@@ -152,7 +202,7 @@ function goalRoundPrompt(authority) {
|
|
|
152
202
|
* until the renderer gives up on incremental repaint. See `live-window.ts`.
|
|
153
203
|
*/
|
|
154
204
|
const LIVE_FURNITURE_ROWS = 10;
|
|
155
|
-
export function App({ ctx: initialCtx }) {
|
|
205
|
+
export function App({ ctx: initialCtx, onExitSummary }) {
|
|
156
206
|
// Bind approval and every operation it admits to one real directory. A
|
|
157
207
|
// lexical cwd may be a writable symlink; resolving it again after the gate
|
|
158
208
|
// would let it be repointed from the approved project to another.
|
|
@@ -271,8 +321,12 @@ export function App({ ctx: initialCtx }) {
|
|
|
271
321
|
* hold the window shut for the length of the next conversation.
|
|
272
322
|
*/
|
|
273
323
|
const settledRef = useRef(0);
|
|
324
|
+
/** Keeps an in-place detail toggle from moving the rows above that detail. */
|
|
325
|
+
const spacerLayoutRef = useRef(null);
|
|
274
326
|
// Complete prompts waiting for the one queue pump that may start a turn.
|
|
275
327
|
const [queued, setQueued] = useState([]);
|
|
328
|
+
/** Return-submitted prompts accepted by the active turn but not yet drained by the SDK. */
|
|
329
|
+
const [pendingSteers, setPendingSteers] = useState([]);
|
|
276
330
|
// Kept synchronous with the rendered queue so a turn's `finally` can decide
|
|
277
331
|
// whether it is truly the last turn. React state captured when the turn
|
|
278
332
|
// started would still say "empty" after the operator queued a follow-up.
|
|
@@ -332,17 +386,30 @@ export function App({ ctx: initialCtx }) {
|
|
|
332
386
|
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
333
387
|
const [choicePicker, setChoicePickerState] = useState(null);
|
|
334
388
|
const choicePickerRef = useRef(null);
|
|
389
|
+
/**
|
|
390
|
+
* Exact chooser React has committed. The synchronous owner ref is needed to
|
|
391
|
+
* block queue/goal work immediately, but it must not accept the Return key
|
|
392
|
+
* that created it before a menu has existed in the rendered tree.
|
|
393
|
+
*/
|
|
394
|
+
const choicePickerCommittedRef = useRef(null);
|
|
335
395
|
const [selectedChoice, setSelectedChoice] = useState(0);
|
|
336
396
|
const setChoicePicker = useCallback((next) => {
|
|
337
397
|
choicePickerRef.current = next;
|
|
398
|
+
if (next === null)
|
|
399
|
+
choicePickerCommittedRef.current = null;
|
|
338
400
|
setChoicePickerState(next);
|
|
339
401
|
}, []);
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
choicePickerCommittedRef.current = choicePicker;
|
|
404
|
+
}, [choicePicker]);
|
|
340
405
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
341
406
|
const composerDraftTokenRef = useRef(0);
|
|
342
407
|
const exitArmedRef = useRef(false);
|
|
343
408
|
const abortRef = useRef(null);
|
|
344
409
|
/** Identity of the turn allowed to notify when it settles. */
|
|
345
410
|
const activeTurnTokenRef = useRef(null);
|
|
411
|
+
/** Exact live-input owner; absent before provider admission and after settlement. */
|
|
412
|
+
const activeTurnInboxRef = useRef(null);
|
|
346
413
|
/** A broken terminal notification is reported once, not after every turn. */
|
|
347
414
|
const notificationFailureShownRef = useRef(false);
|
|
348
415
|
/**
|
|
@@ -404,6 +471,12 @@ export function App({ ctx: initialCtx }) {
|
|
|
404
471
|
// turns attribute to the resumed conversation.
|
|
405
472
|
const sessionsRef = useRef(null);
|
|
406
473
|
const scopeRef = useRef(null);
|
|
474
|
+
const exitWithSummary = useCallback(() => {
|
|
475
|
+
onExitSummary?.({
|
|
476
|
+
...(scopeRef.current?.sessionId ? { conversationId: scopeRef.current.sessionId } : {}),
|
|
477
|
+
});
|
|
478
|
+
exit();
|
|
479
|
+
}, [exit, onExitSummary]);
|
|
407
480
|
/** Durable active state is not permission to spend turns after a restart. */
|
|
408
481
|
const [goalActivation] = useState(() => new SessionGoalActivation());
|
|
409
482
|
const goalDriveInFlightRef = useRef(false);
|
|
@@ -836,15 +909,14 @@ export function App({ ctx: initialCtx }) {
|
|
|
836
909
|
* nobody can see. Everything this path says goes into `pickerNotice`, which
|
|
837
910
|
* the picker renders.
|
|
838
911
|
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
* address from standard input and exists for exactly that machine.
|
|
912
|
+
* The registered browser callback shows a code rather than returning to a
|
|
913
|
+
* local listener. The picker therefore owns the matching paste field; sending a
|
|
914
|
+
* first-run operator to another terminal would make the sign-in technically
|
|
915
|
+
* started here but impossible to finish on the screen that started it.
|
|
844
916
|
*/
|
|
845
917
|
const startLoginFromPicker = useCallback(async (provider, signal) => {
|
|
846
918
|
if (signal.aborted)
|
|
847
|
-
return;
|
|
919
|
+
return 'failed';
|
|
848
920
|
cancelPendingLogin();
|
|
849
921
|
if (provider === 'codex') {
|
|
850
922
|
let start;
|
|
@@ -855,11 +927,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
855
927
|
if (!signal.aborted) {
|
|
856
928
|
setPickerNotice(`Could not start Codex sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
857
929
|
}
|
|
858
|
-
return;
|
|
930
|
+
return 'failed';
|
|
859
931
|
}
|
|
860
932
|
if (signal.aborted) {
|
|
861
933
|
start.cancel();
|
|
862
|
-
return;
|
|
934
|
+
return 'failed';
|
|
863
935
|
}
|
|
864
936
|
codexLoginRef.current = start;
|
|
865
937
|
setPickerNotice(describeCodexDeviceLoginStart({
|
|
@@ -869,7 +941,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
869
941
|
}));
|
|
870
942
|
const outcome = await start.waitForCompletion();
|
|
871
943
|
if (codexLoginRef.current !== start || signal.aborted)
|
|
872
|
-
return;
|
|
944
|
+
return 'failed';
|
|
873
945
|
codexLoginRef.current = null;
|
|
874
946
|
start.cancel();
|
|
875
947
|
setPickerNotice(describeLoginOutcome(outcome));
|
|
@@ -877,7 +949,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
877
949
|
setPickerInitialView('providers');
|
|
878
950
|
await runProbeRef.current?.(signal);
|
|
879
951
|
}
|
|
880
|
-
return;
|
|
952
|
+
return 'finished';
|
|
881
953
|
}
|
|
882
954
|
let start;
|
|
883
955
|
try {
|
|
@@ -885,13 +957,13 @@ export function App({ ctx: initialCtx }) {
|
|
|
885
957
|
}
|
|
886
958
|
catch (err) {
|
|
887
959
|
if (signal.aborted)
|
|
888
|
-
return;
|
|
960
|
+
return 'failed';
|
|
889
961
|
setPickerNotice(`Could not start a sign-in: ${err instanceof Error ? err.message : String(err)}`);
|
|
890
|
-
return;
|
|
962
|
+
return 'failed';
|
|
891
963
|
}
|
|
892
964
|
if (signal.aborted) {
|
|
893
965
|
start.cancel();
|
|
894
|
-
return;
|
|
966
|
+
return 'failed';
|
|
895
967
|
}
|
|
896
968
|
loginRef.current = start;
|
|
897
969
|
const cancelForPicker = () => {
|
|
@@ -906,36 +978,39 @@ export function App({ ctx: initialCtx }) {
|
|
|
906
978
|
signal.addEventListener('abort', cancelForPicker, { once: true });
|
|
907
979
|
if (signal.aborted) {
|
|
908
980
|
cancelForPicker();
|
|
909
|
-
return;
|
|
981
|
+
return 'failed';
|
|
910
982
|
}
|
|
911
983
|
setPickerNotice(describeLoginStart({
|
|
912
984
|
url: start.url,
|
|
913
|
-
loopback: start.loopback,
|
|
914
985
|
browserOpened: openInBrowser(start.url),
|
|
915
|
-
|
|
916
|
-
// slash command is what made the feature unreachable; telling them
|
|
917
|
-
// to run a command in another terminal is something they can do
|
|
918
|
-
// from where they are sitting.
|
|
919
|
-
completionHint: 'run "namzu login" in a terminal and paste it there',
|
|
986
|
+
completionHint: 'paste it below and press enter',
|
|
920
987
|
}));
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
if (
|
|
926
|
-
|
|
988
|
+
return 'awaiting-input';
|
|
989
|
+
}, [cancelPendingLogin]);
|
|
990
|
+
const finishLoginFromPicker = useCallback(async (pasted, signal) => {
|
|
991
|
+
const pending = loginRef.current;
|
|
992
|
+
if (!pending) {
|
|
993
|
+
setPickerNotice('There is no Claude sign-in waiting for this code. Start it again.');
|
|
994
|
+
return 'finished';
|
|
995
|
+
}
|
|
996
|
+
if (!parsePastedInput(pasted).code) {
|
|
997
|
+
setPickerNotice('That does not contain an authorization code. Paste the whole finished address, or the code Claude showed.');
|
|
998
|
+
return 'retry';
|
|
999
|
+
}
|
|
1000
|
+
const outcome = await pending.completeWithPastedCode(pasted);
|
|
1001
|
+
if (loginRef.current !== pending || signal.aborted)
|
|
1002
|
+
return 'finished';
|
|
927
1003
|
loginRef.current = null;
|
|
928
|
-
|
|
1004
|
+
loginAbortCleanupRef.current?.();
|
|
929
1005
|
loginAbortCleanupRef.current = null;
|
|
930
|
-
|
|
931
|
-
if (!outcome.ok && outcome.reason.includes('cancelled'))
|
|
932
|
-
return;
|
|
1006
|
+
pending.cancel();
|
|
933
1007
|
setPickerNotice(describeLoginOutcome(outcome));
|
|
934
1008
|
if (outcome.ok) {
|
|
935
1009
|
setPickerInitialView('providers');
|
|
936
1010
|
await runProbeRef.current?.(signal);
|
|
937
1011
|
}
|
|
938
|
-
|
|
1012
|
+
return 'finished';
|
|
1013
|
+
}, []);
|
|
939
1014
|
const runProbe = useCallback(async (signal) => {
|
|
940
1015
|
try {
|
|
941
1016
|
if (signal?.aborted)
|
|
@@ -1083,8 +1158,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
1083
1158
|
// is idempotent, so a repeated render reaches the same answer.
|
|
1084
1159
|
const window = liveWindow({
|
|
1085
1160
|
messages: finalized,
|
|
1086
|
-
rows:
|
|
1087
|
-
columns:
|
|
1161
|
+
rows: stdout.rows,
|
|
1162
|
+
columns: stdout.columns,
|
|
1088
1163
|
furnitureRows: LIVE_FURNITURE_ROWS,
|
|
1089
1164
|
settled: settledRef.current,
|
|
1090
1165
|
raw: rawOutput,
|
|
@@ -1094,14 +1169,28 @@ export function App({ ctx: initialCtx }) {
|
|
|
1094
1169
|
// the answer is knowable. `liveRows` is the furniture beneath the transcript
|
|
1095
1170
|
// PLUS the live window above it — the window is part of the live region, and
|
|
1096
1171
|
// leaving it out would have the spacer padding room that is already taken.
|
|
1097
|
-
const
|
|
1172
|
+
const spacerCandidate = phase === 'ready'
|
|
1098
1173
|
? bottomSpacerRows({
|
|
1099
|
-
rows:
|
|
1100
|
-
columns:
|
|
1174
|
+
rows: stdout.rows,
|
|
1175
|
+
columns: stdout.columns,
|
|
1101
1176
|
transcript: transcriptLines(finalized.slice(0, window.settled), rawOutput),
|
|
1102
1177
|
liveRows: LIVE_FURNITURE_ROWS + window.rows,
|
|
1103
1178
|
})
|
|
1104
1179
|
: 0;
|
|
1180
|
+
const spacerLayout = {
|
|
1181
|
+
rows: stdout.rows,
|
|
1182
|
+
columns: stdout.columns,
|
|
1183
|
+
raw: rawOutput,
|
|
1184
|
+
messageCount: finalized.length,
|
|
1185
|
+
tail: finalized
|
|
1186
|
+
.slice(-MAX_LIVE_ROWS)
|
|
1187
|
+
.map(({ id, content, meta, detail }) => ({ id, content, meta, detail })),
|
|
1188
|
+
};
|
|
1189
|
+
const spacerRows = phase === 'ready' && sameSpacerLayout(spacerLayoutRef.current, spacerLayout)
|
|
1190
|
+
? spacerLayoutRef.current.spacerRows
|
|
1191
|
+
: spacerCandidate;
|
|
1192
|
+
spacerLayoutRef.current =
|
|
1193
|
+
phase === 'ready' ? { ...spacerLayout, spacerRows } : null;
|
|
1105
1194
|
// One merged vocabulary for the session: this host's own commands plus
|
|
1106
1195
|
// whatever the kernel's registry reports. Built here so `/help`, the
|
|
1107
1196
|
// autocomplete and the dispatcher all answer from the same list — three
|
|
@@ -1214,6 +1303,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
1214
1303
|
goalActivation.disarm(activeSessionId);
|
|
1215
1304
|
wakeGoalDriver();
|
|
1216
1305
|
activeTurnTokenRef.current = null;
|
|
1306
|
+
activeTurnInboxRef.current = null;
|
|
1307
|
+
setPendingSteers([]);
|
|
1217
1308
|
// Dropped now so a second interrupt does not re-abort, and the queue with
|
|
1218
1309
|
// it: interrupting means stop, not "run the next one".
|
|
1219
1310
|
abortRef.current = null;
|
|
@@ -1643,6 +1734,28 @@ export function App({ ctx: initialCtx }) {
|
|
|
1643
1734
|
setActiveTools(activeToolsRef.current);
|
|
1644
1735
|
break;
|
|
1645
1736
|
}
|
|
1737
|
+
case 'tool-progress': {
|
|
1738
|
+
const running = activeToolsRef.current;
|
|
1739
|
+
const index = running.findIndex((tool) => tool.id === event.toolUseId);
|
|
1740
|
+
// A terminal event may already have removed this call. Never recreate
|
|
1741
|
+
// a live row from late diagnostic state.
|
|
1742
|
+
if (index < 0)
|
|
1743
|
+
break;
|
|
1744
|
+
const current = running[index];
|
|
1745
|
+
const { fraction: _fraction, ...withoutFraction } = current;
|
|
1746
|
+
const updated = {
|
|
1747
|
+
...withoutFraction,
|
|
1748
|
+
progress: event.message,
|
|
1749
|
+
...(event.fraction !== undefined ? { fraction: event.fraction } : {}),
|
|
1750
|
+
};
|
|
1751
|
+
activeToolsRef.current = [
|
|
1752
|
+
...running.slice(0, index),
|
|
1753
|
+
updated,
|
|
1754
|
+
...running.slice(index + 1),
|
|
1755
|
+
];
|
|
1756
|
+
setActiveTools(activeToolsRef.current);
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1646
1759
|
case 'tool-end': {
|
|
1647
1760
|
const running = activeToolsRef.current;
|
|
1648
1761
|
// Match strictly by toolUseId. Never fall back to "the first
|
|
@@ -1792,16 +1905,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
1792
1905
|
: undefined);
|
|
1793
1906
|
const priorForSdk = [...historyBeforeTurn, userMessage];
|
|
1794
1907
|
const runId = generateRunId();
|
|
1795
|
-
const metaParts = [];
|
|
1796
|
-
if (attached.length > 0)
|
|
1797
|
-
metaParts.push(`${attached.length} file${attached.length > 1 ? 's' : ''} attached`);
|
|
1798
|
-
if (attachments && attachments.length > 0)
|
|
1799
|
-
metaParts.push(`${attachments.length} attachment${attachments.length > 1 ? 's' : ''}`);
|
|
1800
1908
|
if (goalRound) {
|
|
1801
1909
|
pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
|
|
1802
1910
|
}
|
|
1803
1911
|
else {
|
|
1804
|
-
pushMessage('user', text, false, undefined, undefined, undefined,
|
|
1912
|
+
pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
|
|
1805
1913
|
}
|
|
1806
1914
|
// The model interleaves text → tool → text across iterations; `applyEvent`
|
|
1807
1915
|
// renders each one in order.
|
|
@@ -1843,6 +1951,51 @@ export function App({ ctx: initialCtx }) {
|
|
|
1843
1951
|
const turnGeneration = conversationGenRef.current;
|
|
1844
1952
|
unsettledTurnGenerationsRef.current.set(turnToken, turnGeneration);
|
|
1845
1953
|
const stillHere = () => conversationGenRef.current === turnGeneration;
|
|
1954
|
+
let inboxOpen = true;
|
|
1955
|
+
const inboxEntries = [];
|
|
1956
|
+
const inbox = {
|
|
1957
|
+
accept(input) {
|
|
1958
|
+
if (!inboxOpen ||
|
|
1959
|
+
ac.signal.aborted ||
|
|
1960
|
+
!stillHere() ||
|
|
1961
|
+
activeTurnTokenRef.current !== turnToken ||
|
|
1962
|
+
activeTurnInboxRef.current !== inbox)
|
|
1963
|
+
return false;
|
|
1964
|
+
inboxEntries.push(input);
|
|
1965
|
+
setPendingSteers([...inboxEntries]);
|
|
1966
|
+
return true;
|
|
1967
|
+
},
|
|
1968
|
+
drain() {
|
|
1969
|
+
if (!inboxOpen ||
|
|
1970
|
+
ac.signal.aborted ||
|
|
1971
|
+
activeTurnTokenRef.current !== turnToken ||
|
|
1972
|
+
activeTurnInboxRef.current !== inbox ||
|
|
1973
|
+
inboxEntries.length === 0)
|
|
1974
|
+
return [];
|
|
1975
|
+
const delivered = inboxEntries.splice(0, inboxEntries.length);
|
|
1976
|
+
// This callback runs at the kernel's provider-valid boundary: every
|
|
1977
|
+
// delta from the prior answer has already crossed the event stream.
|
|
1978
|
+
// Commit the user rows here, not at keypress time, so the transcript
|
|
1979
|
+
// cannot place a steer inside an assistant message that is still live.
|
|
1980
|
+
if (stillHere()) {
|
|
1981
|
+
flushStream(st);
|
|
1982
|
+
if (st.assistantId) {
|
|
1983
|
+
finalizeMessage(st.assistantId);
|
|
1984
|
+
st.assistantId = null;
|
|
1985
|
+
}
|
|
1986
|
+
for (const input of delivered) {
|
|
1987
|
+
pushMessage('user', input.prompt.text, false, undefined, undefined, undefined, humanPromptMeta(input.attachedFiles, input.prompt.attachments, true));
|
|
1988
|
+
}
|
|
1989
|
+
setPendingSteers([]);
|
|
1990
|
+
}
|
|
1991
|
+
return delivered.map((input) => input.message);
|
|
1992
|
+
},
|
|
1993
|
+
close() {
|
|
1994
|
+
inboxOpen = false;
|
|
1995
|
+
return inboxEntries.splice(0, inboxEntries.length);
|
|
1996
|
+
},
|
|
1997
|
+
};
|
|
1998
|
+
activeTurnInboxRef.current = inbox;
|
|
1846
1999
|
const turnPermissionMode = permissionModeRef.current;
|
|
1847
2000
|
const turnReasoningEffort = reasoningEffortRef.current;
|
|
1848
2001
|
// Always carry the guarded callback. `auto` and `strict` decide before
|
|
@@ -1906,6 +2059,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
1906
2059
|
...(goalRound ? { goalRound } : {}),
|
|
1907
2060
|
// The mode above decides whether this callback is consulted.
|
|
1908
2061
|
onPermission: askPermission,
|
|
2062
|
+
inboundMessages: () => inbox.drain(),
|
|
1909
2063
|
extraSystem: composeSkillsPrompt(activeSkills) ?? undefined,
|
|
1910
2064
|
onConversationMessages: (messages) => {
|
|
1911
2065
|
// State-only: opaque reasoning/signatures must reach the next
|
|
@@ -1958,6 +2112,12 @@ export function App({ ctx: initialCtx }) {
|
|
|
1958
2112
|
}
|
|
1959
2113
|
}
|
|
1960
2114
|
finally {
|
|
2115
|
+
const undeliveredLiveInput = inbox.close();
|
|
2116
|
+
const ownsInbox = activeTurnInboxRef.current === inbox;
|
|
2117
|
+
if (ownsInbox) {
|
|
2118
|
+
activeTurnInboxRef.current = null;
|
|
2119
|
+
setPendingSteers([]);
|
|
2120
|
+
}
|
|
1961
2121
|
// Prefer the kernel's settled conversation projection: it retains opaque
|
|
1962
2122
|
// reasoning, citations and complete tool turns that the visible delta
|
|
1963
2123
|
// stream cannot reconstruct. A fake/legacy session that does not publish
|
|
@@ -1982,6 +2142,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
1982
2142
|
// clear the state of whatever has started since.
|
|
1983
2143
|
if (stillHere()) {
|
|
1984
2144
|
const ownsTurn = activeTurnTokenRef.current === turnToken;
|
|
2145
|
+
if (ownsInbox && undeliveredLiveInput.length > 0) {
|
|
2146
|
+
replaceQueued(mergeUndeliveredLiveInput(queuedRef.current, undeliveredLiveInput));
|
|
2147
|
+
}
|
|
1985
2148
|
const shouldPauseQueued = ownsTurn &&
|
|
1986
2149
|
abnormalTerminal?.token === turnToken &&
|
|
1987
2150
|
abnormalTerminal.continuationEpoch === queueContinuationEpochRef.current &&
|
|
@@ -2085,12 +2248,13 @@ export function App({ ctx: initialCtx }) {
|
|
|
2085
2248
|
goalActivation,
|
|
2086
2249
|
onPermission,
|
|
2087
2250
|
pushMessage,
|
|
2251
|
+
replaceQueued,
|
|
2088
2252
|
sendTerminalNotification,
|
|
2089
2253
|
session,
|
|
2090
2254
|
setQueuePause,
|
|
2091
2255
|
wakeGoalDriver,
|
|
2092
2256
|
]);
|
|
2093
|
-
const handleSubmit = useCallback((value, attachments) => {
|
|
2257
|
+
const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
|
|
2094
2258
|
if (goalCommandInFlightRef.current) {
|
|
2095
2259
|
pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
|
|
2096
2260
|
return;
|
|
@@ -2128,7 +2292,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2128
2292
|
void startFreshConversation(slash.clearScreen);
|
|
2129
2293
|
return;
|
|
2130
2294
|
case 'exit':
|
|
2131
|
-
|
|
2295
|
+
exitWithSummary();
|
|
2132
2296
|
return;
|
|
2133
2297
|
case 'repick':
|
|
2134
2298
|
// Opened as a choice, not as a repair. A launch-time refusal
|
|
@@ -2669,11 +2833,25 @@ export function App({ ctx: initialCtx }) {
|
|
|
2669
2833
|
// Keep the attachment array beside its text. Reconstructing a prompt from
|
|
2670
2834
|
// the transcript later cannot recover bytes whose composer chip is gone.
|
|
2671
2835
|
advanceQueueContinuation();
|
|
2672
|
-
|
|
2836
|
+
const prompt = {
|
|
2673
2837
|
kind: 'human',
|
|
2674
2838
|
text: outgoing,
|
|
2675
2839
|
...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
|
|
2676
|
-
}
|
|
2840
|
+
};
|
|
2841
|
+
if (mode === 'submit') {
|
|
2842
|
+
const inbox = activeTurnInboxRef.current;
|
|
2843
|
+
if (inbox) {
|
|
2844
|
+
const expanded = expandFileMentions(outgoing, ctx.cwd);
|
|
2845
|
+
if (inbox.accept({
|
|
2846
|
+
prompt,
|
|
2847
|
+
message: createUserMessage(expanded.sendText, prompt.attachments),
|
|
2848
|
+
attachedFiles: expanded.attached.length,
|
|
2849
|
+
queueBoundary: queuedRef.current.length,
|
|
2850
|
+
}))
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
enqueueQueued(prompt);
|
|
2677
2855
|
}, [
|
|
2678
2856
|
activeSkills,
|
|
2679
2857
|
advanceQueueContinuation,
|
|
@@ -2682,7 +2860,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2682
2860
|
ctx.cwd,
|
|
2683
2861
|
doResume,
|
|
2684
2862
|
enqueueQueued,
|
|
2685
|
-
|
|
2863
|
+
exitWithSummary,
|
|
2686
2864
|
hasUnsettledTurn,
|
|
2687
2865
|
nextId,
|
|
2688
2866
|
pushMessage,
|
|
@@ -2987,8 +3165,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2987
3165
|
setPhase('ready');
|
|
2988
3166
|
return;
|
|
2989
3167
|
}
|
|
2990
|
-
|
|
2991
|
-
}, [session,
|
|
3168
|
+
exitWithSummary();
|
|
3169
|
+
}, [session, exitWithSummary]);
|
|
2992
3170
|
useInput((input, key) => {
|
|
2993
3171
|
// The provider picker owns its own keyboard — arrows, digits, enter,
|
|
2994
3172
|
// esc and the typed-key screen are all its business. Ctrl+C is not: it
|
|
@@ -3004,7 +3182,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
3004
3182
|
// and the second exits", with nothing on screen naming either.
|
|
3005
3183
|
if (phase === 'picker') {
|
|
3006
3184
|
if (key.ctrl && input === 'c')
|
|
3007
|
-
|
|
3185
|
+
exitWithSummary();
|
|
3008
3186
|
return;
|
|
3009
3187
|
}
|
|
3010
3188
|
// Previous-prompt picker owns the keyboard. Esc keeps stepping toward
|
|
@@ -3062,7 +3240,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
3062
3240
|
// and making the escape hatch hesitate on the program's first screen
|
|
3063
3241
|
// would read as a hang.
|
|
3064
3242
|
if (ch === 'n' || key.escape || (key.ctrl && input === 'c')) {
|
|
3065
|
-
|
|
3243
|
+
exitWithSummary();
|
|
3066
3244
|
return;
|
|
3067
3245
|
}
|
|
3068
3246
|
// Enter is deliberately NOT trust, for the reason it is not approval
|
|
@@ -3112,7 +3290,14 @@ export function App({ ctx: initialCtx }) {
|
|
|
3112
3290
|
// request and above the ordinary composer. Their values are a captured
|
|
3113
3291
|
// menu, so a provider/session change cannot silently reinterpret Enter.
|
|
3114
3292
|
if (choicePickerRef.current) {
|
|
3115
|
-
const
|
|
3293
|
+
const picker = choicePickerRef.current;
|
|
3294
|
+
// Composer publishes a chooser while handling Return. App receives the
|
|
3295
|
+
// same input dispatch (and key repeat can arrive before the commit), so
|
|
3296
|
+
// the synchronous ref is an ownership fence but not yet an actionable
|
|
3297
|
+
// menu. Only the exact object React committed may consume a choice.
|
|
3298
|
+
if (choicePickerCommittedRef.current !== picker)
|
|
3299
|
+
return;
|
|
3300
|
+
const options = picker.options;
|
|
3116
3301
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
3117
3302
|
setChoicePicker(null);
|
|
3118
3303
|
return;
|
|
@@ -3223,7 +3408,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
3223
3408
|
return;
|
|
3224
3409
|
}
|
|
3225
3410
|
if (exitArmedRef.current) {
|
|
3226
|
-
|
|
3411
|
+
exitWithSummary();
|
|
3227
3412
|
return;
|
|
3228
3413
|
}
|
|
3229
3414
|
exitArmedRef.current = true;
|
|
@@ -3240,46 +3425,49 @@ export function App({ ctx: initialCtx }) {
|
|
|
3240
3425
|
// Background is left natural — we inherit the terminal's own background
|
|
3241
3426
|
// and only theme the foreground. Forcing
|
|
3242
3427
|
// a filled bg left mismatched patches around bordered areas, so we don't.
|
|
3243
|
-
return (
|
|
3244
|
-
|
|
3245
|
-
conversationMutation === null &&
|
|
3246
|
-
choicePicker === null &&
|
|
3247
|
-
copyPicker === null, hidden: permission !== null || choicePicker !== null || copyPicker !== null, children: [queued.length > 0 &&
|
|
3248
|
-
permission === null &&
|
|
3249
|
-
choicePicker === null &&
|
|
3250
|
-
copyPicker === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
3251
|
-
? `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
3252
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
|
|
3253
|
-
state === 'awaiting-permission' ||
|
|
3254
|
-
compacting ||
|
|
3255
|
-
conversationMutation !== null, hidden: permission !== null || choicePicker !== null || copyPicker !== null,
|
|
3256
|
-
// A turn is running, so Esc is the interrupt and not
|
|
3257
|
-
// the composer's clear.
|
|
3258
|
-
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), userCommands: userCommands, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: phase === 'ready' &&
|
|
3259
|
-
state === 'idle' &&
|
|
3428
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: window.settled, spacerRows: spacerRows, resetKey: resetKey, raw: rawOutput, header: phase === 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) }), _jsx(LiveActivity, { activeTools: activeTools, thinking: state === 'thinking' && !messages.some((m) => m.pending) }), permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls })) : choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice })) : copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
|
|
3429
|
+
phase === 'ready' &&
|
|
3260
3430
|
conversationMutation === null &&
|
|
3261
|
-
!compacting &&
|
|
3262
|
-
permission === null &&
|
|
3263
3431
|
choicePicker === null &&
|
|
3264
|
-
copyPicker === null
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
:
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3432
|
+
copyPicker === null, hidden: permission !== null || choicePicker !== null || copyPicker !== null, children: [pendingSteers.length > 0 &&
|
|
3433
|
+
permission === null &&
|
|
3434
|
+
choicePicker === null &&
|
|
3435
|
+
copyPicker === 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 &&
|
|
3436
|
+
permission === null &&
|
|
3437
|
+
choicePicker === null &&
|
|
3438
|
+
copyPicker === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
3439
|
+
? `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
3440
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
|
|
3441
|
+
state === 'awaiting-permission' ||
|
|
3442
|
+
compacting ||
|
|
3443
|
+
conversationMutation !== null, hidden: permission !== null || choicePicker !== null || copyPicker !== null,
|
|
3444
|
+
// A turn is running, so Esc is the interrupt and not
|
|
3445
|
+
// the composer's clear.
|
|
3446
|
+
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), userCommands: userCommands, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: phase === 'ready' &&
|
|
3447
|
+
state === 'idle' &&
|
|
3448
|
+
conversationMutation === null &&
|
|
3449
|
+
!compacting &&
|
|
3450
|
+
permission === null &&
|
|
3451
|
+
choicePicker === null &&
|
|
3452
|
+
copyPicker === null
|
|
3453
|
+
? goalStatusLabel(goalStatus)
|
|
3454
|
+
: null, state: state, hint: conversationMutation === 'fork'
|
|
3455
|
+
? 'forking conversation — input is paused'
|
|
3456
|
+
: conversationMutation === 'edit'
|
|
3457
|
+
? 'branching before prompt — input is paused'
|
|
3458
|
+
: conversationMutation === 'new'
|
|
3459
|
+
? 'starting a fresh conversation — input is paused'
|
|
3460
|
+
: compacting
|
|
3461
|
+
? 'compacting conversation — input is paused'
|
|
3462
|
+
: permission
|
|
3463
|
+
? hintForPhase(phase, state, session?.hasProvider === true)
|
|
3464
|
+
: choicePicker
|
|
3465
|
+
? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
|
|
3466
|
+
: copyPicker
|
|
3467
|
+
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
3468
|
+
: goalStatus && phase === 'ready' && state === 'idle'
|
|
3469
|
+
? undefined
|
|
3470
|
+
: hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] }) }));
|
|
3283
3471
|
}
|
|
3284
3472
|
function Banner({ version, session, permissionMode, cwd, }) {
|
|
3285
3473
|
const cols = process.stdout.columns ?? 80;
|
|
@@ -3334,8 +3522,11 @@ canReturn = false) {
|
|
|
3334
3522
|
if (state === 'awaiting-permission')
|
|
3335
3523
|
return 'y approve · n / esc reject · a approve all';
|
|
3336
3524
|
if (state !== 'idle')
|
|
3337
|
-
return '
|
|
3338
|
-
|
|
3525
|
+
return 'enter steer · tab queue · esc interrupt';
|
|
3526
|
+
// The composer already points to /help. Keeping the complete key legend in
|
|
3527
|
+
// every idle frame made ordinary conversation read like a permanent setup
|
|
3528
|
+
// screen; only state-specific actions belong in the footer.
|
|
3529
|
+
return '';
|
|
3339
3530
|
}
|
|
3340
3531
|
function goalStatusLabel(goal) {
|
|
3341
3532
|
if (!goal)
|