@namzu/cli 14.2.1 → 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 +23 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -1
- package/dist/cli.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/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 +213 -27
- 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.map +1 -1
- package/dist/tui/Picker.js +40 -9
- package/dist/tui/Picker.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/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 +6 -6
package/dist/tui/App.js
CHANGED
|
@@ -17,6 +17,7 @@ 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
23
|
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.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);
|
|
@@ -1085,8 +1158,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
1085
1158
|
// is idempotent, so a repeated render reaches the same answer.
|
|
1086
1159
|
const window = liveWindow({
|
|
1087
1160
|
messages: finalized,
|
|
1088
|
-
rows:
|
|
1089
|
-
columns:
|
|
1161
|
+
rows: stdout.rows,
|
|
1162
|
+
columns: stdout.columns,
|
|
1090
1163
|
furnitureRows: LIVE_FURNITURE_ROWS,
|
|
1091
1164
|
settled: settledRef.current,
|
|
1092
1165
|
raw: rawOutput,
|
|
@@ -1096,14 +1169,28 @@ export function App({ ctx: initialCtx }) {
|
|
|
1096
1169
|
// the answer is knowable. `liveRows` is the furniture beneath the transcript
|
|
1097
1170
|
// PLUS the live window above it — the window is part of the live region, and
|
|
1098
1171
|
// leaving it out would have the spacer padding room that is already taken.
|
|
1099
|
-
const
|
|
1172
|
+
const spacerCandidate = phase === 'ready'
|
|
1100
1173
|
? bottomSpacerRows({
|
|
1101
|
-
rows:
|
|
1102
|
-
columns:
|
|
1174
|
+
rows: stdout.rows,
|
|
1175
|
+
columns: stdout.columns,
|
|
1103
1176
|
transcript: transcriptLines(finalized.slice(0, window.settled), rawOutput),
|
|
1104
1177
|
liveRows: LIVE_FURNITURE_ROWS + window.rows,
|
|
1105
1178
|
})
|
|
1106
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;
|
|
1107
1194
|
// One merged vocabulary for the session: this host's own commands plus
|
|
1108
1195
|
// whatever the kernel's registry reports. Built here so `/help`, the
|
|
1109
1196
|
// autocomplete and the dispatcher all answer from the same list — three
|
|
@@ -1216,6 +1303,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
1216
1303
|
goalActivation.disarm(activeSessionId);
|
|
1217
1304
|
wakeGoalDriver();
|
|
1218
1305
|
activeTurnTokenRef.current = null;
|
|
1306
|
+
activeTurnInboxRef.current = null;
|
|
1307
|
+
setPendingSteers([]);
|
|
1219
1308
|
// Dropped now so a second interrupt does not re-abort, and the queue with
|
|
1220
1309
|
// it: interrupting means stop, not "run the next one".
|
|
1221
1310
|
abortRef.current = null;
|
|
@@ -1645,6 +1734,28 @@ export function App({ ctx: initialCtx }) {
|
|
|
1645
1734
|
setActiveTools(activeToolsRef.current);
|
|
1646
1735
|
break;
|
|
1647
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
|
+
}
|
|
1648
1759
|
case 'tool-end': {
|
|
1649
1760
|
const running = activeToolsRef.current;
|
|
1650
1761
|
// Match strictly by toolUseId. Never fall back to "the first
|
|
@@ -1794,16 +1905,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
1794
1905
|
: undefined);
|
|
1795
1906
|
const priorForSdk = [...historyBeforeTurn, userMessage];
|
|
1796
1907
|
const runId = generateRunId();
|
|
1797
|
-
const metaParts = [];
|
|
1798
|
-
if (attached.length > 0)
|
|
1799
|
-
metaParts.push(`${attached.length} file${attached.length > 1 ? 's' : ''} attached`);
|
|
1800
|
-
if (attachments && attachments.length > 0)
|
|
1801
|
-
metaParts.push(`${attachments.length} attachment${attachments.length > 1 ? 's' : ''}`);
|
|
1802
1908
|
if (goalRound) {
|
|
1803
1909
|
pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
|
|
1804
1910
|
}
|
|
1805
1911
|
else {
|
|
1806
|
-
pushMessage('user', text, false, undefined, undefined, undefined,
|
|
1912
|
+
pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
|
|
1807
1913
|
}
|
|
1808
1914
|
// The model interleaves text → tool → text across iterations; `applyEvent`
|
|
1809
1915
|
// renders each one in order.
|
|
@@ -1845,6 +1951,51 @@ export function App({ ctx: initialCtx }) {
|
|
|
1845
1951
|
const turnGeneration = conversationGenRef.current;
|
|
1846
1952
|
unsettledTurnGenerationsRef.current.set(turnToken, turnGeneration);
|
|
1847
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;
|
|
1848
1999
|
const turnPermissionMode = permissionModeRef.current;
|
|
1849
2000
|
const turnReasoningEffort = reasoningEffortRef.current;
|
|
1850
2001
|
// Always carry the guarded callback. `auto` and `strict` decide before
|
|
@@ -1908,6 +2059,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
1908
2059
|
...(goalRound ? { goalRound } : {}),
|
|
1909
2060
|
// The mode above decides whether this callback is consulted.
|
|
1910
2061
|
onPermission: askPermission,
|
|
2062
|
+
inboundMessages: () => inbox.drain(),
|
|
1911
2063
|
extraSystem: composeSkillsPrompt(activeSkills) ?? undefined,
|
|
1912
2064
|
onConversationMessages: (messages) => {
|
|
1913
2065
|
// State-only: opaque reasoning/signatures must reach the next
|
|
@@ -1960,6 +2112,12 @@ export function App({ ctx: initialCtx }) {
|
|
|
1960
2112
|
}
|
|
1961
2113
|
}
|
|
1962
2114
|
finally {
|
|
2115
|
+
const undeliveredLiveInput = inbox.close();
|
|
2116
|
+
const ownsInbox = activeTurnInboxRef.current === inbox;
|
|
2117
|
+
if (ownsInbox) {
|
|
2118
|
+
activeTurnInboxRef.current = null;
|
|
2119
|
+
setPendingSteers([]);
|
|
2120
|
+
}
|
|
1963
2121
|
// Prefer the kernel's settled conversation projection: it retains opaque
|
|
1964
2122
|
// reasoning, citations and complete tool turns that the visible delta
|
|
1965
2123
|
// stream cannot reconstruct. A fake/legacy session that does not publish
|
|
@@ -1984,6 +2142,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
1984
2142
|
// clear the state of whatever has started since.
|
|
1985
2143
|
if (stillHere()) {
|
|
1986
2144
|
const ownsTurn = activeTurnTokenRef.current === turnToken;
|
|
2145
|
+
if (ownsInbox && undeliveredLiveInput.length > 0) {
|
|
2146
|
+
replaceQueued(mergeUndeliveredLiveInput(queuedRef.current, undeliveredLiveInput));
|
|
2147
|
+
}
|
|
1987
2148
|
const shouldPauseQueued = ownsTurn &&
|
|
1988
2149
|
abnormalTerminal?.token === turnToken &&
|
|
1989
2150
|
abnormalTerminal.continuationEpoch === queueContinuationEpochRef.current &&
|
|
@@ -2087,12 +2248,13 @@ export function App({ ctx: initialCtx }) {
|
|
|
2087
2248
|
goalActivation,
|
|
2088
2249
|
onPermission,
|
|
2089
2250
|
pushMessage,
|
|
2251
|
+
replaceQueued,
|
|
2090
2252
|
sendTerminalNotification,
|
|
2091
2253
|
session,
|
|
2092
2254
|
setQueuePause,
|
|
2093
2255
|
wakeGoalDriver,
|
|
2094
2256
|
]);
|
|
2095
|
-
const handleSubmit = useCallback((value, attachments) => {
|
|
2257
|
+
const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
|
|
2096
2258
|
if (goalCommandInFlightRef.current) {
|
|
2097
2259
|
pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
|
|
2098
2260
|
return;
|
|
@@ -2130,7 +2292,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2130
2292
|
void startFreshConversation(slash.clearScreen);
|
|
2131
2293
|
return;
|
|
2132
2294
|
case 'exit':
|
|
2133
|
-
|
|
2295
|
+
exitWithSummary();
|
|
2134
2296
|
return;
|
|
2135
2297
|
case 'repick':
|
|
2136
2298
|
// Opened as a choice, not as a repair. A launch-time refusal
|
|
@@ -2671,11 +2833,25 @@ export function App({ ctx: initialCtx }) {
|
|
|
2671
2833
|
// Keep the attachment array beside its text. Reconstructing a prompt from
|
|
2672
2834
|
// the transcript later cannot recover bytes whose composer chip is gone.
|
|
2673
2835
|
advanceQueueContinuation();
|
|
2674
|
-
|
|
2836
|
+
const prompt = {
|
|
2675
2837
|
kind: 'human',
|
|
2676
2838
|
text: outgoing,
|
|
2677
2839
|
...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
|
|
2678
|
-
}
|
|
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);
|
|
2679
2855
|
}, [
|
|
2680
2856
|
activeSkills,
|
|
2681
2857
|
advanceQueueContinuation,
|
|
@@ -2684,7 +2860,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2684
2860
|
ctx.cwd,
|
|
2685
2861
|
doResume,
|
|
2686
2862
|
enqueueQueued,
|
|
2687
|
-
|
|
2863
|
+
exitWithSummary,
|
|
2688
2864
|
hasUnsettledTurn,
|
|
2689
2865
|
nextId,
|
|
2690
2866
|
pushMessage,
|
|
@@ -2989,8 +3165,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2989
3165
|
setPhase('ready');
|
|
2990
3166
|
return;
|
|
2991
3167
|
}
|
|
2992
|
-
|
|
2993
|
-
}, [session,
|
|
3168
|
+
exitWithSummary();
|
|
3169
|
+
}, [session, exitWithSummary]);
|
|
2994
3170
|
useInput((input, key) => {
|
|
2995
3171
|
// The provider picker owns its own keyboard — arrows, digits, enter,
|
|
2996
3172
|
// esc and the typed-key screen are all its business. Ctrl+C is not: it
|
|
@@ -3006,7 +3182,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
3006
3182
|
// and the second exits", with nothing on screen naming either.
|
|
3007
3183
|
if (phase === 'picker') {
|
|
3008
3184
|
if (key.ctrl && input === 'c')
|
|
3009
|
-
|
|
3185
|
+
exitWithSummary();
|
|
3010
3186
|
return;
|
|
3011
3187
|
}
|
|
3012
3188
|
// Previous-prompt picker owns the keyboard. Esc keeps stepping toward
|
|
@@ -3064,7 +3240,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
3064
3240
|
// and making the escape hatch hesitate on the program's first screen
|
|
3065
3241
|
// would read as a hang.
|
|
3066
3242
|
if (ch === 'n' || key.escape || (key.ctrl && input === 'c')) {
|
|
3067
|
-
|
|
3243
|
+
exitWithSummary();
|
|
3068
3244
|
return;
|
|
3069
3245
|
}
|
|
3070
3246
|
// Enter is deliberately NOT trust, for the reason it is not approval
|
|
@@ -3114,7 +3290,14 @@ export function App({ ctx: initialCtx }) {
|
|
|
3114
3290
|
// request and above the ordinary composer. Their values are a captured
|
|
3115
3291
|
// menu, so a provider/session change cannot silently reinterpret Enter.
|
|
3116
3292
|
if (choicePickerRef.current) {
|
|
3117
|
-
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;
|
|
3118
3301
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
3119
3302
|
setChoicePicker(null);
|
|
3120
3303
|
return;
|
|
@@ -3225,7 +3408,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
3225
3408
|
return;
|
|
3226
3409
|
}
|
|
3227
3410
|
if (exitArmedRef.current) {
|
|
3228
|
-
|
|
3411
|
+
exitWithSummary();
|
|
3229
3412
|
return;
|
|
3230
3413
|
}
|
|
3231
3414
|
exitArmedRef.current = true;
|
|
@@ -3242,11 +3425,14 @@ export function App({ ctx: initialCtx }) {
|
|
|
3242
3425
|
// Background is left natural — we inherit the terminal's own background
|
|
3243
3426
|
// and only theme the foreground. Forcing
|
|
3244
3427
|
// a filled bg left mismatched patches around bordered areas, so we don't.
|
|
3245
|
-
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, resetKey: resetKey, raw: rawOutput, header: phase === 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) }),
|
|
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' &&
|
|
3246
3429
|
phase === 'ready' &&
|
|
3247
3430
|
conversationMutation === null &&
|
|
3248
3431
|
choicePicker === null &&
|
|
3249
|
-
copyPicker === null, hidden: permission !== null || choicePicker !== null || copyPicker !== null, children: [
|
|
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 &&
|
|
3250
3436
|
permission === null &&
|
|
3251
3437
|
choicePicker === null &&
|
|
3252
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
|
|
@@ -3336,7 +3522,7 @@ canReturn = false) {
|
|
|
3336
3522
|
if (state === 'awaiting-permission')
|
|
3337
3523
|
return 'y approve · n / esc reject · a approve all';
|
|
3338
3524
|
if (state !== 'idle')
|
|
3339
|
-
return '
|
|
3525
|
+
return 'enter steer · tab queue · esc interrupt';
|
|
3340
3526
|
// The composer already points to /help. Keeping the complete key legend in
|
|
3341
3527
|
// every idle frame made ordinary conversation read like a permanent setup
|
|
3342
3528
|
// screen; only state-specific actions belong in the footer.
|