mindweave 3.3.0 → 3.4.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/App.js +120 -22
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/components/FadeText.js +47 -0
- package/dist/cli/components/FadeText.js.map +1 -0
- package/dist/cli/components/PromptInput.js +22 -1
- package/dist/cli/components/PromptInput.js.map +1 -1
- package/dist/cli/components/ToolLine.js +5 -3
- package/dist/cli/components/ToolLine.js.map +1 -1
- package/dist/cli/diffRows.js +16 -0
- package/dist/cli/diffRows.js.map +1 -1
- package/dist/cli/entrance.js +95 -0
- package/dist/cli/entrance.js.map +1 -0
- package/dist/cli/framebuffer/parse.js +99 -16
- package/dist/cli/framebuffer/parse.js.map +1 -1
- package/dist/cli/framebuffer/writer.js +46 -28
- package/dist/cli/framebuffer/writer.js.map +1 -1
- package/dist/cli/help.js +6 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/drivers/anthropic/client.js +16 -3
- package/dist/drivers/anthropic/client.js.map +1 -1
- package/dist/drivers/anthropic/manifest.js +10 -0
- package/dist/drivers/anthropic/manifest.js.map +1 -1
- package/dist/drivers/glm/manifest.js +9 -0
- package/dist/drivers/glm/manifest.js.map +1 -1
- package/dist/drivers/groq/manifest.js +5 -0
- package/dist/drivers/groq/manifest.js.map +1 -1
- package/dist/drivers/mistral/client.js +3 -0
- package/dist/drivers/mistral/client.js.map +1 -1
- package/dist/drivers/openai/client.js +3 -0
- package/dist/drivers/openai/client.js.map +1 -1
- package/dist/drivers/openai/manifest.js +5 -0
- package/dist/drivers/openai/manifest.js.map +1 -1
- package/dist/drivers/openaiCompat/wire.js +7 -2
- package/dist/drivers/openaiCompat/wire.js.map +1 -1
- package/dist/drivers/openrouter/client.js +6 -0
- package/dist/drivers/openrouter/client.js.map +1 -1
- package/dist/drivers/xai/client.js +9 -0
- package/dist/drivers/xai/client.js.map +1 -1
- package/dist/dynamo/contextWindow.js +64 -12
- package/dist/dynamo/contextWindow.js.map +1 -1
- package/dist/dynamo/engine.js +64 -18
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/memory/compaction.js +3 -1
- package/dist/memory/compaction.js.map +1 -1
- package/dist/memory/localCommand.js +30 -3
- package/dist/memory/localCommand.js.map +1 -1
- package/dist/memory/session.js +4 -0
- package/dist/memory/session.js.map +1 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/tools/backgroundShells.js +35 -7
- package/dist/tools/backgroundShells.js.map +1 -1
- package/dist/tools/deferredNative.js +24 -0
- package/dist/tools/deferredNative.js.map +1 -1
- package/dist/tools/mcpSearch.js +8 -2
- package/dist/tools/mcpSearch.js.map +1 -1
- package/dist/tools/registry.js +12 -12
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/runCommand.js +9 -2
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/shellTools.js +10 -23
- package/dist/tools/shellTools.js.map +1 -1
- package/dist/tools/untrusted.js +24 -0
- package/dist/tools/untrusted.js.map +1 -1
- package/package.json +3 -3
package/dist/cli/App.js
CHANGED
|
@@ -109,7 +109,7 @@ import { countNewReplies, hitsPill, pillBounds, scrollPill } from "./scrollPill.
|
|
|
109
109
|
import { SCROLLBACK_BLOCKS, virtualWindow } from "./virtualWindow.js";
|
|
110
110
|
import { perf, perfEnabled } from "./perfLog.js";
|
|
111
111
|
import { newPacer, nextHoldAt, nextMove, takeImmediate, takePaced } from "./revealQueue.js";
|
|
112
|
-
import { drain as drainQueue, popAll as popAllQueued, queueMessage, takeSteerable
|
|
112
|
+
import { drain as drainQueue, popAll as popAllQueued, queueMessage, takeSteerable } from "./messageQueue.js";
|
|
113
113
|
import { routeCommand, parseCommandLine, unknownCommandMessage } from "./commandRoute.js";
|
|
114
114
|
import { resolveChoice, splitModelArg } from "./commandArgs.js";
|
|
115
115
|
import { carryAcrossFreshSession } from "./sessionCarry.js";
|
|
@@ -359,6 +359,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
359
359
|
/** The chip's cells in SCREEN coordinates, for the pointer handler. Null when there is
|
|
360
360
|
* nothing to click. Published by the render, read between frames. */
|
|
361
361
|
const pillHit = useRef(null);
|
|
362
|
+
/** The queued line's row within the live region (measured as it is drawn), and its screen row for a click. */
|
|
363
|
+
const queueRow = useRef(null);
|
|
364
|
+
// Steered messages that left the queue but have not reached the chat yet (see steerRunningTurn).
|
|
365
|
+
const [departing, setDeparting] = useState([]);
|
|
366
|
+
const departingRef = useRef(0);
|
|
367
|
+
const queueHitRow = useRef(null);
|
|
368
|
+
/** Pulls the queue back into the input box to edit (installed by the input, as the caret click is). */
|
|
369
|
+
const pullQueue = useRef(null);
|
|
362
370
|
// Where the layout's first row sits on the screen, published each render for the pointer.
|
|
363
371
|
const frameTopRef = useRef(0);
|
|
364
372
|
// The row the pointer is over, if it can be pressed. State rather than a ref: it is what
|
|
@@ -1823,6 +1831,12 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1823
1831
|
// On PRESS rather than release: the chip is a single row and the view moves out
|
|
1824
1832
|
// from under the pointer the instant it is hit, so a release-matched-to-press
|
|
1825
1833
|
// would be testing the pointer against a screen that had already changed.
|
|
1834
|
+
// The queued line is a button too: a click on it takes the queue back into the box to edit.
|
|
1835
|
+
if (queueHitRow.current !== null && event.y === queueHitRow.current && pullQueue.current) {
|
|
1836
|
+
clearSelection();
|
|
1837
|
+
pullQueue.current();
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1826
1840
|
const hit = pillHit.current;
|
|
1827
1841
|
if (hit && hitsPill(hit, event.x, event.y)) {
|
|
1828
1842
|
clearSelection();
|
|
@@ -2228,6 +2242,8 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2228
2242
|
holdTimer.current = null;
|
|
2229
2243
|
revealQ.current = [];
|
|
2230
2244
|
pacer.current = newPacer(revealQ.current);
|
|
2245
|
+
departingRef.current = 0;
|
|
2246
|
+
setDeparting([]);
|
|
2231
2247
|
}
|
|
2232
2248
|
function enqueueReveal(a) {
|
|
2233
2249
|
revealQ.current.push(a);
|
|
@@ -2277,6 +2293,13 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2277
2293
|
// same de-sync risk a reveal carries, so the banner is healed on the same frame.
|
|
2278
2294
|
requestFullRepaint();
|
|
2279
2295
|
applyBatch(actions);
|
|
2296
|
+
// A steered message that has just reached the chat stops showing in the queue line.
|
|
2297
|
+
const arrived = departingRef.current > 0 ? actions.filter((a) => a.type === "user").length : 0;
|
|
2298
|
+
if (arrived > 0) {
|
|
2299
|
+
const n = Math.min(arrived, departingRef.current);
|
|
2300
|
+
departingRef.current -= n;
|
|
2301
|
+
setDeparting((d) => d.slice(n));
|
|
2302
|
+
}
|
|
2280
2303
|
}
|
|
2281
2304
|
function pace() {
|
|
2282
2305
|
return {
|
|
@@ -2364,23 +2387,35 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2364
2387
|
* rows around it.
|
|
2365
2388
|
*/
|
|
2366
2389
|
async function steerRunningTurn(s) {
|
|
2390
|
+
// Never into a turn that has been stopped (Esc, ctrl+F, an interrupted command). Handed over at its
|
|
2391
|
+
// last step boundary, a message queued BEFORE the stop was drawn in the chat, put in the transcript
|
|
2392
|
+
// and never answered, because nothing runs after a stopped turn. It stays queued and goes out as
|
|
2393
|
+
// the next turn, which is what the person who typed it expects.
|
|
2394
|
+
if (abortRef.current?.signal.aborted)
|
|
2395
|
+
return [];
|
|
2367
2396
|
const { send, rest } = takeSteerable(queueRef.current);
|
|
2368
2397
|
if (send.length === 0)
|
|
2369
2398
|
return [];
|
|
2370
2399
|
queueRef.current = rest;
|
|
2371
|
-
setQueued(rest);
|
|
2372
2400
|
const out = [];
|
|
2401
|
+
const shown = [];
|
|
2373
2402
|
for (const { text } of send) {
|
|
2374
2403
|
// No history write here: `onSend` records every message the moment it is typed,
|
|
2375
2404
|
// queued or not, so ↑ walks them in the order they were written rather than the
|
|
2376
2405
|
// order they happened to go out. Recording again on the way out appended each one
|
|
2377
2406
|
// a second time.
|
|
2378
2407
|
const { content, displayText, notes, images } = await prepareMessage(s, text);
|
|
2379
|
-
|
|
2380
|
-
for (const n of notes)
|
|
2381
|
-
enqueueReveal({ type: "note", text: n });
|
|
2408
|
+
shown.push({ type: "user", text: displayText }, ...notes.map((n) => ({ type: "note", text: n })));
|
|
2382
2409
|
out.push({ content, ...(images.length > 0 ? { images } : {}) });
|
|
2383
2410
|
}
|
|
2411
|
+
// The message leaves the queue line in the same paint it reaches the chat, so it is never on
|
|
2412
|
+
// neither. Behind other rows it waits its beat in the chat's order, so the queue line keeps
|
|
2413
|
+
// showing it (`departing`) until then.
|
|
2414
|
+
setDeparting((d) => [...d, ...send]);
|
|
2415
|
+
departingRef.current += send.length;
|
|
2416
|
+
setQueued([...queueRef.current]);
|
|
2417
|
+
for (const a of shown)
|
|
2418
|
+
enqueueReveal(a);
|
|
2384
2419
|
return out;
|
|
2385
2420
|
}
|
|
2386
2421
|
async function streamRespond(s, run) {
|
|
@@ -3890,6 +3925,36 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3890
3925
|
}
|
|
3891
3926
|
void handleSubmit(text);
|
|
3892
3927
|
}
|
|
3928
|
+
/**
|
|
3929
|
+
* ctrl+F: send NOW, the way the queue sends, at the first moment that loses nothing.
|
|
3930
|
+
*
|
|
3931
|
+
* It is the queue, not a second mechanism: the message goes in the same queue and reaches the agent
|
|
3932
|
+
* by the same road (handed to the running turn when its step ends). What it adds is a way to make
|
|
3933
|
+
* that moment come SOONER, and the only thing that holds a step up for long without anyone's
|
|
3934
|
+
* leave is a command the turn is waiting on. That is moved to the background, as ctrl+X moves it:
|
|
3935
|
+
* it keeps running, the step ends at once, the message arrives, and the agent is told the command
|
|
3936
|
+
* is still going and hears when it ends. Nothing is stopped, killed or thrown away; only Esc, or
|
|
3937
|
+
* the person asking, does that.
|
|
3938
|
+
*
|
|
3939
|
+
* A reply being written, a compaction or a sub-agent has no such handle and is left alone: the
|
|
3940
|
+
* message arrives the moment it ends, exactly as it would have through Enter.
|
|
3941
|
+
*/
|
|
3942
|
+
function forceSend(text) {
|
|
3943
|
+
if (!busy && queueRef.current.length === 0) {
|
|
3944
|
+
// Idle: nothing is in the way, so it is Enter.
|
|
3945
|
+
if (text)
|
|
3946
|
+
onSend(text);
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
if (text) {
|
|
3950
|
+
setScrollUp(0);
|
|
3951
|
+
setHistory((h) => (h[h.length - 1] === text ? h : [...h, text]));
|
|
3952
|
+
queueRef.current.push(queueMessage(text, { interrupting: interrupting.current }));
|
|
3953
|
+
setQueued([...queueRef.current]);
|
|
3954
|
+
}
|
|
3955
|
+
if (busy)
|
|
3956
|
+
pressBackground(session.current?.toolContext.foregroundCommands);
|
|
3957
|
+
}
|
|
3893
3958
|
// Autocomplete entries: the built-in slash commands, this project's skills, and any
|
|
3894
3959
|
// prompts the connected MCP servers offer (all read from the live session, so a freshly
|
|
3895
3960
|
// created skill — or a server that just finished connecting — shows up next render).
|
|
@@ -3942,36 +4007,36 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3942
4007
|
return null;
|
|
3943
4008
|
const cur = session.current;
|
|
3944
4009
|
if (overlay.kind === "app") {
|
|
3945
|
-
return (_jsx(Picker, { title: "Mindweave desktop app", note: APP_EXPLANATION, items: [{ label: "Open the download page", description: "opens your default browser" }], width: width, maxRows: maxRows, initialIndex: 0, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4010
|
+
return (_jsx(Picker, { title: "Mindweave desktop app", note: APP_EXPLANATION, items: [{ label: "Open the download page", description: "opens your default browser" }], width: width, maxRows: maxRows, initialIndex: 0, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3946
4011
|
}
|
|
3947
4012
|
if (overlay.kind === "rewind") {
|
|
3948
4013
|
const items = overlay.items.map((p) => ({
|
|
3949
4014
|
label: p.text,
|
|
3950
4015
|
description: [timeAgo(p.at), changeSummary(p)].filter(Boolean).join(" · "),
|
|
3951
4016
|
}));
|
|
3952
|
-
return (_jsx(Picker, { title: "Rewind to before which message?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4017
|
+
return (_jsx(Picker, { title: "Rewind to before which message?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3953
4018
|
}
|
|
3954
4019
|
if (overlay.kind === "rewindConfirm") {
|
|
3955
4020
|
const p = overlay.point;
|
|
3956
4021
|
const quoted = p.text.length > 48 ? `${p.text.slice(0, 47)}…` : p.text;
|
|
3957
|
-
return (_jsx(Picker, { title: `Rewind to before “${quoted}”?`, items: rewindChoices(p).map(({ label, description }) => ({ label, description })), describeSelection: true, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4022
|
+
return (_jsx(Picker, { title: `Rewind to before “${quoted}”?`, items: rewindChoices(p).map(({ label, description }) => ({ label, description })), describeSelection: true, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3958
4023
|
}
|
|
3959
4024
|
if (overlay.kind === "sessions") {
|
|
3960
4025
|
const items = overlay.items.map((m) => ({
|
|
3961
4026
|
label: sessionTitle(m),
|
|
3962
4027
|
description: `${timeAgo(m.updatedAt)} · ${m.entryCount} msg${m.entryCount === 1 ? "" : "s"}`,
|
|
3963
4028
|
}));
|
|
3964
|
-
return (_jsx(Picker, { title: "Continue which session?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4029
|
+
return (_jsx(Picker, { title: "Continue which session?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3965
4030
|
}
|
|
3966
4031
|
if (overlay.kind === "resumeMode") {
|
|
3967
|
-
return (_jsx(Picker, { title: `Continue “${sessionTitle(overlay.meta)}” — how?`, items: RESUME_MODES, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4032
|
+
return (_jsx(Picker, { title: `Continue “${sessionTitle(overlay.meta)}” — how?`, items: RESUME_MODES, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3968
4033
|
}
|
|
3969
4034
|
if (overlay.kind === "shells") {
|
|
3970
4035
|
const items = overlay.items.map((sh) => ({
|
|
3971
4036
|
label: `#${sh.id} ${clipCmd(sh.command)}`,
|
|
3972
4037
|
description: sh.status === "running" ? `running ${shellElapsed(sh)} — Enter to stop` : sh.status === "killed" ? "killed" : `exited ${sh.exitCode}`,
|
|
3973
4038
|
}));
|
|
3974
|
-
return (_jsx(Picker, { title: "Background shells", items: items, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4039
|
+
return (_jsx(Picker, { title: "Background shells", items: items, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3975
4040
|
}
|
|
3976
4041
|
if (overlay.kind === "provider") {
|
|
3977
4042
|
const activeModel = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
@@ -3992,7 +4057,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3992
4057
|
const here = p.id === active ? ` · on ${activeLabel}` : "";
|
|
3993
4058
|
return { label: p.label + (p.id === active ? " ✓" : ""), description: `${models} · ${key}${here}` };
|
|
3994
4059
|
});
|
|
3995
|
-
return (_jsx(Picker, { title: "Choose a provider", items: items, width: width, maxRows: maxRows, searchBox: providerSearch, initialIndex: Math.max(0, providers.findIndex((p) => p.id === active)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4060
|
+
return (_jsx(Picker, { title: "Choose a provider", items: items, width: width, maxRows: maxRows, searchBox: providerSearch, initialIndex: Math.max(0, providers.findIndex((p) => p.id === active)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3996
4061
|
}
|
|
3997
4062
|
if (overlay.kind === "model") {
|
|
3998
4063
|
const id = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
@@ -4016,14 +4081,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4016
4081
|
keywords: m.id,
|
|
4017
4082
|
};
|
|
4018
4083
|
});
|
|
4019
|
-
return (_jsx(Picker, { title: `Choose ${/^[aeiou]/i.test(pickerProvider) ? "an" : "a"} ${pickerProvider} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), initialFilter: overlay.filter, searchBox: modelSearch, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4084
|
+
return (_jsx(Picker, { title: `Choose ${/^[aeiou]/i.test(pickerProvider) ? "an" : "a"} ${pickerProvider} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), initialFilter: overlay.filter, searchBox: modelSearch, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
4020
4085
|
}
|
|
4021
4086
|
if (overlay.kind === "think") {
|
|
4022
4087
|
const model = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
4023
4088
|
const levels = thinkLevels(model);
|
|
4024
4089
|
const curLabel = cur ? thinkLabel(cur.modelConfig) : "";
|
|
4025
4090
|
const items = levels.map((l) => ({ label: l.label + (l.label === curLabel ? " ✓" : ""), description: l.description }));
|
|
4026
|
-
return (_jsx(Picker, { title: `Reasoning for ${modelLabel(model)}`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, levels.findIndex((l) => l.label === curLabel)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4091
|
+
return (_jsx(Picker, { title: `Reasoning for ${modelLabel(model)}`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, levels.findIndex((l) => l.label === curLabel)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
4027
4092
|
}
|
|
4028
4093
|
if (overlay.kind === "screen") {
|
|
4029
4094
|
const choices = screenChoices(shell);
|
|
@@ -4033,7 +4098,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4033
4098
|
describeSelection: true,
|
|
4034
4099
|
// Opens on the one in use, so Enter alone changes nothing. A chooser that opens
|
|
4035
4100
|
// somewhere else turns a glance at the options into an accidental switch.
|
|
4036
|
-
initialIndex: Math.max(0, choices.findIndex((c) => c.mode === shell)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4101
|
+
initialIndex: Math.max(0, choices.findIndex((c) => c.mode === shell)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
4037
4102
|
}
|
|
4038
4103
|
// approval — a plan, a Sentinel action, a forbidden-path lift. It interrupts the user's
|
|
4039
4104
|
// work and the answer commits them to something, so it reads as a stop; it renders in
|
|
@@ -4180,6 +4245,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4180
4245
|
const frameTop = readingInline ? Math.max(0, rows - frameHeight) : 0;
|
|
4181
4246
|
frameTopRef.current = frameTop;
|
|
4182
4247
|
pillHit.current = pill !== null && pillRow.current !== null ? pillBounds(pill, width, frameTop + pillRow.current) : null;
|
|
4248
|
+
queueHitRow.current = queueRow.current !== null ? frameTop + queueRow.current : null;
|
|
4183
4249
|
// The whole conversation stays reachable: the cap is only a safety valve (see
|
|
4184
4250
|
// SCROLLBACK_BLOCKS). Blocks outside the window cost one number each here and nothing
|
|
4185
4251
|
// in Yoga, which only ever sees the window and its spacers.
|
|
@@ -4281,10 +4347,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4281
4347
|
// The status line, the queued bar, the input and whatever sits under it. Built once
|
|
4282
4348
|
// and rendered by BOTH shells: the only thing that differs between them is where it
|
|
4283
4349
|
// ends up — pinned to the bottom of a frame we own, or simply the last thing printed.
|
|
4284
|
-
const footerView = (_jsxs(Box, { ref: footerRef, flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { flexShrink: 0,
|
|
4350
|
+
const footerView = (_jsxs(Box, { ref: footerRef, flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { flexShrink: 0, marginBottom: queued.length + departing.length > 0 ? 1 : 0, ref: (node) => {
|
|
4351
|
+
queueRow.current = node && queued.length + departing.length > 0 ? measureElement(node).y : null;
|
|
4352
|
+
}, children: _jsx(QueuedBar, { queued: [...departing, ...queued] }) }), _jsx(Box, { flexShrink: 0, children: _jsx(StatusLine, { busy: busy, userCommand: busy && userTurn.current, startedAt: turnStart.current, lastMs: lastMs, usage: taskUsage, received: liveTokens, advance: advanceTokens, waiting: overlay?.kind === "approval", waited: () => waitedMs.current + (waitSince.current != null ? Date.now() - waitSince.current : 0) }) }), marathonUi ? (_jsx(Box, { flexShrink: 0, children: _jsx(MarathonBox, { ui: marathonUi, width: width, cap: shell === "inline" ? 4 : 7 }) })) : null, _jsx(Box, { flexShrink: 0, flexDirection: "column", children: ready ? (_jsx(PromptInput, { onSubmit: onSend, onForceSend: forceSend, opening: opening, disabled: false, placeholder: marathonUi?.phase === "armed" ? "describe the goal…" : busy ? "type to queue a message…" : "say something…", width: width, history: history, completions: completions, pathComplete: pathComplete.current, onLargePaste: registerPaste, onDroppedPaths: (text) => dropHandles.current.register(text), registerCaretClick: (place) => {
|
|
4285
4353
|
caretClick.current = place;
|
|
4286
4354
|
}, registerTextSelect: (select) => {
|
|
4287
4355
|
textSelect.current = select;
|
|
4356
|
+
}, registerQueuePull: (pull) => {
|
|
4357
|
+
pullQueue.current = pull;
|
|
4288
4358
|
}, maxMenuRows: maxMenuItems, menuAbove: shell === "inline", settleKey: committed.length, onMenuChange: onMenuChange, placeCursor: shell === "inline", onQueuePop: popQueue, overlay: overlayView, ...(searchPlaceholder ? { searchPlaceholder } : {}), fill: fill })) : (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "starting\u2026" }) })) }), ctxWarn ? (
|
|
4289
4359
|
// A compaction is near. This takes the line over the tip and the background bar:
|
|
4290
4360
|
// it is the one thing here the user needs BEFORE it happens, since the summarizing
|
|
@@ -4789,16 +4859,27 @@ function StatusLine({ busy, userCommand, startedAt, lastMs, usage, received, adv
|
|
|
4789
4859
|
// rather than as jumping, and the working dot moves smoothly. It was 50ms (20 frames a
|
|
4790
4860
|
// second), which was the slowest-looking thing on a screen that could draw three times as
|
|
4791
4861
|
// often, and at 1Hz before that it moved once a second in whatever lump had arrived, which
|
|
4792
|
-
// is a stutter, not an animation.
|
|
4793
|
-
//
|
|
4794
|
-
//
|
|
4795
|
-
//
|
|
4862
|
+
// is a stutter, not an animation. Only the easing advances on every tick; a render is asked for
|
|
4863
|
+
// only when the line's text would change (see below), because a tick that draws the same text
|
|
4864
|
+
// still costs a whole pass through layout and the terminal writer, about a millisecond each,
|
|
4865
|
+
// sixty times a second, for the whole time the agent works.
|
|
4796
4866
|
const [, tick] = useState(0);
|
|
4867
|
+
// The line as the last render drew it, and the line as it would be drawn now. The clock below runs
|
|
4868
|
+
// at the display's pace to keep the counter easing smoothly, but a render that would draw the same
|
|
4869
|
+
// text is a whole pass through layout and the terminal writer for nothing, and most ticks are that:
|
|
4870
|
+
// the seconds change once a second and the count only when the eased figure moves a whole step.
|
|
4871
|
+
// Read through a ref so the interval never has to be torn down when the props change.
|
|
4872
|
+
const drawn = useRef("");
|
|
4873
|
+
const nowKey = useRef(() => "");
|
|
4874
|
+
nowKey.current = () => (busy && startedAt != null ? statusKey(userCommand, waiting, startedAt, waited(), received()) : "");
|
|
4875
|
+
drawn.current = nowKey.current();
|
|
4797
4876
|
useEffect(() => {
|
|
4798
4877
|
if (!busy)
|
|
4799
4878
|
return;
|
|
4800
4879
|
const id = setInterval(() => {
|
|
4801
4880
|
advance();
|
|
4881
|
+
if (nowKey.current() === drawn.current)
|
|
4882
|
+
return;
|
|
4802
4883
|
tick((t) => t + 1);
|
|
4803
4884
|
}, LIVE_TICK_MS);
|
|
4804
4885
|
return () => clearInterval(id);
|
|
@@ -4849,6 +4930,16 @@ const DOUBLE_CLICK_MS = 500;
|
|
|
4849
4930
|
const EDGE_SCROLL_MS = 45;
|
|
4850
4931
|
/** The working line's render clock, in milliseconds. See StatusLine's tick. */
|
|
4851
4932
|
const LIVE_TICK_MS = 16;
|
|
4933
|
+
/**
|
|
4934
|
+
* Everything the working line's text is made of, as one string (pure). Two equal keys draw the same
|
|
4935
|
+
* line, so a render for an unchanged key can be skipped. Built from the same calls the line uses.
|
|
4936
|
+
*/
|
|
4937
|
+
export function statusKey(userCommand, waiting, startedAt, waitedMs, got) {
|
|
4938
|
+
const secs = workedSeconds(Date.now(), startedAt, waitedMs);
|
|
4939
|
+
if (userCommand)
|
|
4940
|
+
return `u|${fmtElapsed(secs)}`;
|
|
4941
|
+
return `${waiting ? "w" : workingVerb(startedAt, Date.now() - startedAt - waitedMs)}|${fmtElapsed(secs)}|${got > 0 ? formatTokens(got) : ""}`;
|
|
4942
|
+
}
|
|
4852
4943
|
function fmtElapsed(s) {
|
|
4853
4944
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
4854
4945
|
}
|
|
@@ -4914,11 +5005,18 @@ function bgClock(ms) {
|
|
|
4914
5005
|
* you cannot see the handle on — ↑ is not a thing anyone guesses, and the cost of not
|
|
4915
5006
|
* guessing it is a message you no longer wanted being sent anyway.
|
|
4916
5007
|
*/
|
|
5008
|
+
/**
|
|
5009
|
+
* What is waiting to be sent, on ONE line shaped like a work row: the marker, what it is, a dot
|
|
5010
|
+
* between the parts, the key that sends it, and the arrow that opens it (a click pulls the queue back
|
|
5011
|
+
* into the box to edit, as the up key does). Long text is cut so the key and the arrow always stay
|
|
5012
|
+
* on the line.
|
|
5013
|
+
*/
|
|
4917
5014
|
function QueuedBar({ queued }) {
|
|
4918
5015
|
if (queued.length === 0)
|
|
4919
5016
|
return null;
|
|
4920
|
-
const
|
|
4921
|
-
|
|
5017
|
+
const first = queued[0].text.replace(/\s+/g, " ").trim();
|
|
5018
|
+
const more = queued.length - 1;
|
|
5019
|
+
return (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { dimColor: true, children: "● " }), _jsx(Box, { flexShrink: 1, children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["queued: ", first] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: [more > 0 ? ` · ${more} more` : "", " · ctrl+F to send now ", "▸"] }) })] }));
|
|
4922
5020
|
}
|
|
4923
5021
|
/**
|
|
4924
5022
|
* Whether some OTHER installed provider has a key, so `/provider` is worth
|