@namzu/cli 18.0.0 → 18.1.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +1 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +1 -0
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +1 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +15 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +17 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +44 -0
- package/dist/context/doctrine.d.ts.map +1 -0
- package/dist/context/doctrine.js +81 -0
- package/dist/context/doctrine.js.map +1 -0
- package/dist/context/turn-snapshot.d.ts +59 -0
- package/dist/context/turn-snapshot.d.ts.map +1 -0
- package/dist/context/turn-snapshot.js +122 -0
- package/dist/context/turn-snapshot.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +13 -0
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +71 -11
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/permissions/mode.d.ts +25 -0
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +11 -1
- package/dist/permissions/mode.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +243 -7
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Composer.d.ts +6 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +18 -2
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +16 -1
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/TaskList.d.ts +30 -0
- package/dist/tui/TaskList.d.ts.map +1 -0
- package/dist/tui/TaskList.js +58 -0
- package/dist/tui/TaskList.js.map +1 -0
- package/dist/tui/agent.d.ts +45 -4
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +165 -18
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +151 -1
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/stream-blocks.d.ts +24 -0
- package/dist/tui/stream-blocks.d.ts.map +1 -1
- package/dist/tui/stream-blocks.js +74 -0
- package/dist/tui/stream-blocks.js.map +1 -1
- package/dist/tui/types.d.ts +3 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/tui/App.js
CHANGED
|
@@ -34,6 +34,7 @@ import { Composer, suggestionWindowSize, } from './Composer.js';
|
|
|
34
34
|
import { CopyPicker } from './CopyPicker.js';
|
|
35
35
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
36
36
|
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
37
|
+
import { TaskList } from './TaskList.js';
|
|
37
38
|
import { PermissionOverlay } from './PermissionOverlay.js';
|
|
38
39
|
import { Picker } from './Picker.js';
|
|
39
40
|
import { ResumePicker } from './ResumePicker.js';
|
|
@@ -57,12 +58,36 @@ import { PERMISSION_REVIEW_PAGE_ROWS, buildPermissionReview, buildPermissionSumm
|
|
|
57
58
|
import { describeRunInterruption } from './run-interruption.js';
|
|
58
59
|
import { moveSelection } from './selection-window.js';
|
|
59
60
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
60
|
-
import { splitCompleteBlocks } from './stream-blocks.js';
|
|
61
|
+
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
61
62
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
62
63
|
import { theme } from './theme.js';
|
|
63
64
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
64
65
|
import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
|
|
65
66
|
import { listReviewBranches, listReviewCommits, reviewMergeBase, } from './workspace-review.js';
|
|
67
|
+
/** The value a question row carries: an option id, or the free-text escape. */
|
|
68
|
+
const FREE_TEXT_ANSWER = '__free_text__';
|
|
69
|
+
/**
|
|
70
|
+
* The streaming assistant bubble, carried across events within one turn.
|
|
71
|
+
*
|
|
72
|
+
* `text` is everything the model has said and is what gets persisted;
|
|
73
|
+
* `pending` is the part not yet on screen, held until it forms a whole block.
|
|
74
|
+
* The two are separate because they answer different questions — what was
|
|
75
|
+
* said, and what has been shown — and conflating them is how a reply gets
|
|
76
|
+
* saved with a paragraph the operator never saw, or shown twice.
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* How long held reply text may wait before a sentence of it is shown.
|
|
80
|
+
*
|
|
81
|
+
* Blocks are released as they complete (`splitCompleteBlocks`), which keeps a
|
|
82
|
+
* fast reply landing a paragraph at a time. A slow one — a long paragraph
|
|
83
|
+
* arriving over twenty seconds — completes no block until its last character
|
|
84
|
+
* and was invisible for its whole length. Past this interval the pending
|
|
85
|
+
* text is released to its last safe cut instead (`splitSafeCut`): a sentence
|
|
86
|
+
* or a line, never a half-word, never inside a fence. Long enough that a
|
|
87
|
+
* fast stream never reaches it; short enough that a slow one reads as
|
|
88
|
+
* writing rather than as silence.
|
|
89
|
+
*/
|
|
90
|
+
const STREAM_RELEASE_MS = 250;
|
|
66
91
|
/** Last non-empty assistant text in a durable conversation, newest first. */
|
|
67
92
|
function latestAssistantOutput(messages) {
|
|
68
93
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -292,6 +317,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
292
317
|
*/
|
|
293
318
|
const [pickerNotice, setPickerNotice] = useState(null);
|
|
294
319
|
const [permission, setPermission] = useState(null);
|
|
320
|
+
/**
|
|
321
|
+
* The question currently on screen and the promise the tool is waiting
|
|
322
|
+
* on. Resolved exactly once: by a chosen row, by free text, by Esc (skip)
|
|
323
|
+
* or by Ctrl+C (abort). A second resolution is a no-op, not a second
|
|
324
|
+
* answer.
|
|
325
|
+
*/
|
|
326
|
+
const questionRef = useRef(null);
|
|
327
|
+
const resolveQuestion = useCallback((answer) => {
|
|
328
|
+
const pending = questionRef.current;
|
|
329
|
+
if (!pending?.resolve)
|
|
330
|
+
return;
|
|
331
|
+
const resolve = pending.resolve;
|
|
332
|
+
pending.resolve = null;
|
|
333
|
+
questionRef.current = null;
|
|
334
|
+
resolve(answer);
|
|
335
|
+
}, []);
|
|
295
336
|
const [permissionReviewOffset, setPermissionReviewOffsetState] = useState(0);
|
|
296
337
|
const permissionReviewOffsetRef = useRef(0);
|
|
297
338
|
const [permissionDetailsOpen, setPermissionDetailsOpenState] = useState(false);
|
|
@@ -329,6 +370,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
329
370
|
// Tools currently executing — rendered live (spinner + elapsed) below the
|
|
330
371
|
// transcript, then committed as static lines on completion.
|
|
331
372
|
const [activeTools, setActiveTools] = useState([]);
|
|
373
|
+
/**
|
|
374
|
+
* The model's plan for the current request, in creation order, updated in
|
|
375
|
+
* place on every status change. Cleared when the next request begins —
|
|
376
|
+
* a plan is scoped to the request it was made for, and one that outlived
|
|
377
|
+
* it would read as work still pending on a turn that already ended.
|
|
378
|
+
*/
|
|
379
|
+
const [tasks, setTasks] = useState([]);
|
|
380
|
+
/** Ids already seen this request, so an opening row is written once. */
|
|
381
|
+
const knownTaskIdsRef = useRef(new Set());
|
|
332
382
|
// Bumped to reset the <Static> transcript log (on /clear, /clear-screen and /resume).
|
|
333
383
|
const [resetKey, setResetKey] = useState(0);
|
|
334
384
|
/**
|
|
@@ -1027,6 +1077,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1027
1077
|
}
|
|
1028
1078
|
return { sessions, sessionId };
|
|
1029
1079
|
}, [hasUnsettledTurn, pushMessage, state]);
|
|
1080
|
+
/**
|
|
1081
|
+
* Shift+Tab: `prompt` → `accept-edits` → `plan` → `prompt`. Three stops
|
|
1082
|
+
* only — `auto` and `strict` are deliberate choices made by name in
|
|
1083
|
+
* `/permissions`, not stops on a key an operator presses on reflex. From
|
|
1084
|
+
* either of those the key returns to `prompt`, which is the direction a
|
|
1085
|
+
* reflex should fall. The change goes through the same gate `/permissions`
|
|
1086
|
+
* uses, so it is refused while a turn is active, and the refusal is
|
|
1087
|
+
* explained on screen.
|
|
1088
|
+
*/
|
|
1089
|
+
const cyclePermissionMode = useCallback(() => {
|
|
1090
|
+
const current = permissionModeRef.current;
|
|
1091
|
+
const next = current === 'prompt' ? 'accept-edits' : current === 'accept-edits' ? 'plan' : 'prompt';
|
|
1092
|
+
applyPermissionMode(next);
|
|
1093
|
+
}, [applyPermissionMode]);
|
|
1030
1094
|
const runConversationExport = useCallback((destination) => {
|
|
1031
1095
|
const source = stableExportSource();
|
|
1032
1096
|
if (!source)
|
|
@@ -1209,6 +1273,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1209
1273
|
})();
|
|
1210
1274
|
return;
|
|
1211
1275
|
}
|
|
1276
|
+
if (picker.kind === 'user-question') {
|
|
1277
|
+
const pending = questionRef.current;
|
|
1278
|
+
setChoicePicker(null);
|
|
1279
|
+
if (!pending)
|
|
1280
|
+
return;
|
|
1281
|
+
if (value === FREE_TEXT_ANSWER) {
|
|
1282
|
+
// The scope on a text prompt is what its other kinds check
|
|
1283
|
+
// against a conversation switch; a question belongs to the turn
|
|
1284
|
+
// that asked it, and the turn holds the promise.
|
|
1285
|
+
const scope = scopeRef.current;
|
|
1286
|
+
if (!scope) {
|
|
1287
|
+
resolveQuestion({ kind: 'skip' });
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
textPromptTokenRef.current += 1;
|
|
1291
|
+
setTextPrompt({
|
|
1292
|
+
token: textPromptTokenRef.current,
|
|
1293
|
+
kind: 'user-question',
|
|
1294
|
+
title: pending.question.question,
|
|
1295
|
+
placeholder: 'Type your answer and press Enter · Esc skips',
|
|
1296
|
+
emptyNotice: 'An empty answer is a skip. Press Esc to skip.',
|
|
1297
|
+
initialValue: '',
|
|
1298
|
+
sessionId: scope.sessionId,
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// The tool's own result row carries the answer the model saw; this
|
|
1303
|
+
// row is the operator's, so a reader of the transcript sees the
|
|
1304
|
+
// question and the choice together where it happened.
|
|
1305
|
+
pushMessage('system', `Answered "${pending.question.question}": ${labelOfOption(pending.question, value)}`, false, '?');
|
|
1306
|
+
resolveQuestion({ kind: 'answer', selectedOptionIds: [value] });
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1212
1309
|
if (picker.kind === 'review-commit') {
|
|
1213
1310
|
advanceQueueContinuation();
|
|
1214
1311
|
enqueueQueued({
|
|
@@ -1348,12 +1445,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1348
1445
|
* if every exit releases it; this is the function every exit calls.
|
|
1349
1446
|
*/
|
|
1350
1447
|
const flushStream = useCallback((st) => {
|
|
1448
|
+
if (st.releaseTimer) {
|
|
1449
|
+
clearTimeout(st.releaseTimer);
|
|
1450
|
+
st.releaseTimer = undefined;
|
|
1451
|
+
}
|
|
1351
1452
|
if (!st.pending || st.pending.length === 0)
|
|
1352
1453
|
return;
|
|
1353
1454
|
const id = st.assistantId ?? pushMessage('assistant', '', true);
|
|
1354
1455
|
st.assistantId = id;
|
|
1355
1456
|
appendToMessage(id, st.pending);
|
|
1356
1457
|
st.pending = '';
|
|
1458
|
+
st.releasedAt = Date.now();
|
|
1459
|
+
}, [appendToMessage, pushMessage]);
|
|
1460
|
+
/**
|
|
1461
|
+
* Release held text to its last safe cut — a sentence or a line — when it
|
|
1462
|
+
* has waited longer than `STREAM_RELEASE_MS`. Called from the delta path
|
|
1463
|
+
* with the clock, and from the timer the delta path arms so a stream that
|
|
1464
|
+
* goes quiet mid-paragraph still shows what it has.
|
|
1465
|
+
*/
|
|
1466
|
+
const releaseStalled = useCallback((st) => {
|
|
1467
|
+
if (!st.pending || st.pending.length === 0)
|
|
1468
|
+
return;
|
|
1469
|
+
const { ready, rest } = splitSafeCut(st.pending);
|
|
1470
|
+
if (ready.length === 0)
|
|
1471
|
+
return;
|
|
1472
|
+
const id = st.assistantId ?? pushMessage('assistant', '', true);
|
|
1473
|
+
st.assistantId = id;
|
|
1474
|
+
st.pending = rest;
|
|
1475
|
+
appendToMessage(id, ready);
|
|
1476
|
+
st.releasedAt = Date.now();
|
|
1357
1477
|
}, [appendToMessage, pushMessage]);
|
|
1358
1478
|
const finalizeMessage = useCallback((id, finalContent) => {
|
|
1359
1479
|
setMessages((prev) => prev.map((m) => m.id === id ? { ...m, content: finalContent ?? m.content, pending: false } : m));
|
|
@@ -1452,7 +1572,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1452
1572
|
...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
|
|
1453
1573
|
...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
|
|
1454
1574
|
...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
|
|
1575
|
+
...(activeCtx.web ? { web: activeCtx.web } : {}),
|
|
1455
1576
|
...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
|
|
1577
|
+
// Somebody is at this terminal, so the model may ask them one
|
|
1578
|
+
// question when a decision is genuinely theirs.
|
|
1579
|
+
askUser: true,
|
|
1456
1580
|
});
|
|
1457
1581
|
if (signal?.aborted) {
|
|
1458
1582
|
void s.close();
|
|
@@ -2243,6 +2367,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2243
2367
|
if (!prompt)
|
|
2244
2368
|
return;
|
|
2245
2369
|
setTextPrompt(null);
|
|
2370
|
+
if (prompt.kind === 'user-question') {
|
|
2371
|
+
const text = value.trim();
|
|
2372
|
+
const pending = questionRef.current;
|
|
2373
|
+
if (text.length === 0) {
|
|
2374
|
+
resolveQuestion({ kind: 'skip' });
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (pending) {
|
|
2378
|
+
pushMessage('system', `Answered "${pending.question.question}": ${text}`, false, '?');
|
|
2379
|
+
}
|
|
2380
|
+
resolveQuestion({ kind: 'answer', selectedOptionIds: [], freeText: text });
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2246
2383
|
const scope = scopeRef.current;
|
|
2247
2384
|
if (!scope || scope.sessionId !== prompt.sessionId) {
|
|
2248
2385
|
pushMessage('system', prompt.kind === 'conversation-title'
|
|
@@ -2275,8 +2412,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2275
2412
|
queueMicrotask(() => {
|
|
2276
2413
|
if (textPromptRef.current === prompt)
|
|
2277
2414
|
setTextPrompt(null);
|
|
2415
|
+
// A cancelled free-text answer is a skipped question, not a hung tool.
|
|
2416
|
+
if (prompt?.kind === 'user-question')
|
|
2417
|
+
resolveQuestion({ kind: 'skip' });
|
|
2278
2418
|
});
|
|
2279
|
-
}, [setTextPrompt]);
|
|
2419
|
+
}, [resolveQuestion, setTextPrompt]);
|
|
2280
2420
|
/**
|
|
2281
2421
|
* `/fork`: continue in a copy, leaving this conversation where it is.
|
|
2282
2422
|
*
|
|
@@ -2461,6 +2601,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2461
2601
|
sendTerminalNotification({ kind: 'approval-required' });
|
|
2462
2602
|
});
|
|
2463
2603
|
}, [sendTerminalNotification, setPermissionDetailsOpen, setPermissionReviewOffset]);
|
|
2604
|
+
/**
|
|
2605
|
+
* `ask_user_question`, on screen. The question becomes a finite chooser —
|
|
2606
|
+
* the model's options, then "Something else…" when it allowed free text —
|
|
2607
|
+
* and the tool's promise waits on the row the operator picks. One question
|
|
2608
|
+
* at a time: the tool is not concurrency-safe, so a second cannot arrive
|
|
2609
|
+
* while the first is up.
|
|
2610
|
+
*/
|
|
2611
|
+
const askQuestion = useCallback((question) => new Promise((resolve) => {
|
|
2612
|
+
questionRef.current = { question, resolve };
|
|
2613
|
+
const values = [
|
|
2614
|
+
...question.options.map((option) => option.id),
|
|
2615
|
+
...(question.allowFreeText ? [FREE_TEXT_ANSWER] : []),
|
|
2616
|
+
];
|
|
2617
|
+
setSelectedChoice(0);
|
|
2618
|
+
setChoicePicker({
|
|
2619
|
+
kind: 'user-question',
|
|
2620
|
+
title: question.question,
|
|
2621
|
+
notice: question.header
|
|
2622
|
+
? `${question.header} · Enter answers · Esc skips`
|
|
2623
|
+
: 'Enter answers · Esc skips',
|
|
2624
|
+
values,
|
|
2625
|
+
options: [
|
|
2626
|
+
...question.options.map((option) => ({
|
|
2627
|
+
label: option.label,
|
|
2628
|
+
description: option.description ?? '',
|
|
2629
|
+
})),
|
|
2630
|
+
...(question.allowFreeText
|
|
2631
|
+
? [{ label: 'Something else…', description: 'Answer in your own words' }]
|
|
2632
|
+
: []),
|
|
2633
|
+
],
|
|
2634
|
+
});
|
|
2635
|
+
sendTerminalNotification({ kind: 'approval-required' });
|
|
2636
|
+
}), [sendTerminalNotification, setChoicePicker, setSelectedChoice]);
|
|
2464
2637
|
// Render one agent event onto the transcript. Shared by the local turn loop
|
|
2465
2638
|
// and the daemon-attach poller, so both paths produce identical output.
|
|
2466
2639
|
// `st` carries the streaming-assistant bubble id + accumulated text across
|
|
@@ -2491,6 +2664,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2491
2664
|
sessionId: st.sessionId,
|
|
2492
2665
|
};
|
|
2493
2666
|
}
|
|
2667
|
+
// The release clock starts with the first character, not with the
|
|
2668
|
+
// turn: the wait before the first token is the model's, and
|
|
2669
|
+
// counting it would release the opening sentence on arrival.
|
|
2670
|
+
st.releasedAt ??= Date.now();
|
|
2494
2671
|
st.text += event.text;
|
|
2495
2672
|
// Held, not appended. Appending each delta is what produced text
|
|
2496
2673
|
// that types itself out — nothing animates it, but a few
|
|
@@ -2501,7 +2678,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2501
2678
|
if (ready.length > 0) {
|
|
2502
2679
|
st.pending = rest;
|
|
2503
2680
|
appendToMessage(ensureAssistant(), ready);
|
|
2681
|
+
st.releasedAt = Date.now();
|
|
2504
2682
|
}
|
|
2683
|
+
// A block is the preferred unit; a sentence is the fallback for
|
|
2684
|
+
// a paragraph that is taking longer than an operator will wait
|
|
2685
|
+
// for it. The timer covers the stream going quiet between two
|
|
2686
|
+
// deltas, which the clock check alone cannot see.
|
|
2687
|
+
if (st.releaseTimer)
|
|
2688
|
+
clearTimeout(st.releaseTimer);
|
|
2689
|
+
if (Date.now() - (st.releasedAt ?? 0) >= STREAM_RELEASE_MS) {
|
|
2690
|
+
releaseStalled(st);
|
|
2691
|
+
}
|
|
2692
|
+
st.releaseTimer = setTimeout(() => {
|
|
2693
|
+
st.releaseTimer = undefined;
|
|
2694
|
+
releaseStalled(st);
|
|
2695
|
+
}, STREAM_RELEASE_MS);
|
|
2505
2696
|
break;
|
|
2506
2697
|
}
|
|
2507
2698
|
case 'tool-start': {
|
|
@@ -2582,9 +2773,34 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2582
2773
|
case 'usage':
|
|
2583
2774
|
setUsage({ totalTokens: event.totalTokens, cost: event.cost });
|
|
2584
2775
|
break;
|
|
2585
|
-
case 'task':
|
|
2586
|
-
|
|
2776
|
+
case 'task': {
|
|
2777
|
+
// The live list gets every change; the transcript records the
|
|
2778
|
+
// opening and the close, as it did before the list existed.
|
|
2779
|
+
// Decided from a ref, not inside the state updater: React runs
|
|
2780
|
+
// updaters lazily, so a flag set there is still unset when the
|
|
2781
|
+
// transcript row below is chosen.
|
|
2782
|
+
const isNew = !knownTaskIdsRef.current.has(event.taskId);
|
|
2783
|
+
knownTaskIdsRef.current.add(event.taskId);
|
|
2784
|
+
const item = {
|
|
2785
|
+
id: event.taskId,
|
|
2786
|
+
subject: event.subject,
|
|
2787
|
+
status: event.status,
|
|
2788
|
+
};
|
|
2789
|
+
setTasks((prev) => {
|
|
2790
|
+
const index = prev.findIndex((task) => task.id === item.id);
|
|
2791
|
+
return index < 0 ? [...prev, item] : prev.map((task, i) => (i === index ? item : task));
|
|
2792
|
+
});
|
|
2793
|
+
if (event.status === 'completed') {
|
|
2794
|
+
pushMessage('tool', event.subject, false, '☑');
|
|
2795
|
+
}
|
|
2796
|
+
else if (event.status === 'failed') {
|
|
2797
|
+
pushMessage('tool', event.subject, false, '☒');
|
|
2798
|
+
}
|
|
2799
|
+
else if (isNew) {
|
|
2800
|
+
pushMessage('tool', event.subject, false, '☐');
|
|
2801
|
+
}
|
|
2587
2802
|
break;
|
|
2803
|
+
}
|
|
2588
2804
|
case 'context':
|
|
2589
2805
|
// Into the TRANSCRIPT, not a status line. A status indicator is
|
|
2590
2806
|
// present while nothing is happening and gone afterwards, so
|
|
@@ -2721,6 +2937,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2721
2937
|
else {
|
|
2722
2938
|
pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
|
|
2723
2939
|
}
|
|
2940
|
+
// A plan is scoped to the request it was made for. Cleared here, at
|
|
2941
|
+
// the start of the next one, rather than when the previous turn
|
|
2942
|
+
// ended: a finished list stays on screen until the operator moves on,
|
|
2943
|
+
// which is the moment it has told them everything it can.
|
|
2944
|
+
setTasks([]);
|
|
2945
|
+
knownTaskIdsRef.current = new Set();
|
|
2724
2946
|
// The model interleaves text → tool → text across iterations; `applyEvent`
|
|
2725
2947
|
// renders each one in order.
|
|
2726
2948
|
const st = {
|
|
@@ -2870,6 +3092,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2870
3092
|
...(goalRound ? { goalRound } : {}),
|
|
2871
3093
|
// The mode above decides whether this callback is consulted.
|
|
2872
3094
|
onPermission: askPermission,
|
|
3095
|
+
onQuestion: askQuestion,
|
|
2873
3096
|
inboundMessages: () => inbox.drain(),
|
|
2874
3097
|
extraSystem: composeSkillsPrompt(activeSkills) ?? undefined,
|
|
2875
3098
|
onConversationMessages: (messages) => {
|
|
@@ -3218,7 +3441,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3218
3441
|
pushMessage('system', 'Permission choices are unavailable until the active turn, prompt, compaction, queued work, or embedded-session approval latch can be safely settled.');
|
|
3219
3442
|
return;
|
|
3220
3443
|
}
|
|
3221
|
-
const values = ['prompt', 'auto', 'strict'];
|
|
3444
|
+
const values = ['prompt', 'accept-edits', 'auto', 'strict', 'plan'];
|
|
3222
3445
|
setSelectedChoice(Math.max(0, values.indexOf(permissionModeRef.current)));
|
|
3223
3446
|
setChoicePicker({
|
|
3224
3447
|
kind: 'permission-mode',
|
|
@@ -4410,6 +4633,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4410
4633
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
4411
4634
|
reviewChoiceInFlightRef.current = null;
|
|
4412
4635
|
setChoicePicker(null);
|
|
4636
|
+
// Esc leaves the question unanswered and the turn running; the
|
|
4637
|
+
// tool tells the model so. Ctrl+C is the operator saying stop.
|
|
4638
|
+
if (picker.kind === 'user-question') {
|
|
4639
|
+
resolveQuestion({ kind: key.escape ? 'skip' : 'abort' });
|
|
4640
|
+
}
|
|
4413
4641
|
return;
|
|
4414
4642
|
}
|
|
4415
4643
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
@@ -4620,7 +4848,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4620
4848
|
: goalStatus && phase === 'ready' && state === 'idle'
|
|
4621
4849
|
? undefined
|
|
4622
4850
|
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
4623
|
-
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, 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: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
|
|
4851
|
+
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, 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: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, agentSurface === null && permission === null ? _jsx(TaskList, { tasks: tasks }) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
|
|
4624
4852
|
phase === 'ready' &&
|
|
4625
4853
|
conversationMutation === null &&
|
|
4626
4854
|
textPrompt === null &&
|
|
@@ -4640,7 +4868,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4640
4868
|
? queuePause.outcome === 'paused'
|
|
4641
4869
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
4642
4870
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
4643
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
|
|
4871
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode: permissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
|
|
4644
4872
|
state === 'awaiting-permission' ||
|
|
4645
4873
|
compacting ||
|
|
4646
4874
|
conversationMutation !== null ||
|
|
@@ -4731,10 +4959,18 @@ function goalStatusLabel(goal) {
|
|
|
4731
4959
|
return 'Goal achieved';
|
|
4732
4960
|
}
|
|
4733
4961
|
}
|
|
4962
|
+
/** The label the operator saw for the option id they chose. */
|
|
4963
|
+
function labelOfOption(question, optionId) {
|
|
4964
|
+
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
4965
|
+
}
|
|
4734
4966
|
function permissionModeDescription(mode) {
|
|
4735
4967
|
switch (mode) {
|
|
4736
4968
|
case 'prompt':
|
|
4737
4969
|
return 'Ask before an undecided tool call runs';
|
|
4970
|
+
case 'accept-edits':
|
|
4971
|
+
return 'Approve file edits and writes without asking; shell and everything else still ask';
|
|
4972
|
+
case 'plan':
|
|
4973
|
+
return 'Read-only: the agent explores and presents a plan; leave plan mode to carry it out';
|
|
4738
4974
|
case 'auto':
|
|
4739
4975
|
return 'Approve undecided calls unless a rule or safety gate refuses';
|
|
4740
4976
|
case 'strict':
|