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.
Files changed (64) hide show
  1. package/dist/cli/App.js +120 -22
  2. package/dist/cli/App.js.map +1 -1
  3. package/dist/cli/components/FadeText.js +47 -0
  4. package/dist/cli/components/FadeText.js.map +1 -0
  5. package/dist/cli/components/PromptInput.js +22 -1
  6. package/dist/cli/components/PromptInput.js.map +1 -1
  7. package/dist/cli/components/ToolLine.js +5 -3
  8. package/dist/cli/components/ToolLine.js.map +1 -1
  9. package/dist/cli/diffRows.js +16 -0
  10. package/dist/cli/diffRows.js.map +1 -1
  11. package/dist/cli/entrance.js +95 -0
  12. package/dist/cli/entrance.js.map +1 -0
  13. package/dist/cli/framebuffer/parse.js +99 -16
  14. package/dist/cli/framebuffer/parse.js.map +1 -1
  15. package/dist/cli/framebuffer/writer.js +46 -28
  16. package/dist/cli/framebuffer/writer.js.map +1 -1
  17. package/dist/cli/help.js +6 -0
  18. package/dist/cli/help.js.map +1 -1
  19. package/dist/drivers/anthropic/client.js +16 -3
  20. package/dist/drivers/anthropic/client.js.map +1 -1
  21. package/dist/drivers/anthropic/manifest.js +10 -0
  22. package/dist/drivers/anthropic/manifest.js.map +1 -1
  23. package/dist/drivers/glm/manifest.js +9 -0
  24. package/dist/drivers/glm/manifest.js.map +1 -1
  25. package/dist/drivers/groq/manifest.js +5 -0
  26. package/dist/drivers/groq/manifest.js.map +1 -1
  27. package/dist/drivers/mistral/client.js +3 -0
  28. package/dist/drivers/mistral/client.js.map +1 -1
  29. package/dist/drivers/openai/client.js +3 -0
  30. package/dist/drivers/openai/client.js.map +1 -1
  31. package/dist/drivers/openai/manifest.js +5 -0
  32. package/dist/drivers/openai/manifest.js.map +1 -1
  33. package/dist/drivers/openaiCompat/wire.js +7 -2
  34. package/dist/drivers/openaiCompat/wire.js.map +1 -1
  35. package/dist/drivers/openrouter/client.js +6 -0
  36. package/dist/drivers/openrouter/client.js.map +1 -1
  37. package/dist/drivers/xai/client.js +9 -0
  38. package/dist/drivers/xai/client.js.map +1 -1
  39. package/dist/dynamo/contextWindow.js +64 -12
  40. package/dist/dynamo/contextWindow.js.map +1 -1
  41. package/dist/dynamo/engine.js +64 -18
  42. package/dist/dynamo/engine.js.map +1 -1
  43. package/dist/memory/compaction.js +3 -1
  44. package/dist/memory/compaction.js.map +1 -1
  45. package/dist/memory/localCommand.js +30 -3
  46. package/dist/memory/localCommand.js.map +1 -1
  47. package/dist/memory/session.js +4 -0
  48. package/dist/memory/session.js.map +1 -1
  49. package/dist/memory/types.js.map +1 -1
  50. package/dist/tools/backgroundShells.js +35 -7
  51. package/dist/tools/backgroundShells.js.map +1 -1
  52. package/dist/tools/deferredNative.js +24 -0
  53. package/dist/tools/deferredNative.js.map +1 -1
  54. package/dist/tools/mcpSearch.js +8 -2
  55. package/dist/tools/mcpSearch.js.map +1 -1
  56. package/dist/tools/registry.js +12 -12
  57. package/dist/tools/registry.js.map +1 -1
  58. package/dist/tools/runCommand.js +9 -2
  59. package/dist/tools/runCommand.js.map +1 -1
  60. package/dist/tools/shellTools.js +10 -23
  61. package/dist/tools/shellTools.js.map +1 -1
  62. package/dist/tools/untrusted.js +24 -0
  63. package/dist/tools/untrusted.js.map +1 -1
  64. 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, visibleQueue } from "./messageQueue.js";
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
- enqueueReveal({ type: "user", text: displayText });
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, 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) }) }), _jsx(Box, { flexShrink: 0, children: _jsx(QueuedBar, { queued: queued }) }), 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, 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) => {
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. A tick costs about a millisecond now that unchanged rows
4793
- // are not rebuilt (see inkOutputPatch.ts). Nothing else re-renders with it: the tick is this
4794
- // component's own state and the figure is read through a getter, so the cost is one
4795
- // small subtree per frame.
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 { rows, hidden } = visibleQueue(queued);
4921
- return (_jsxs(Box, { flexDirection: "column", children: [rows.map((q, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["⏎ queued: ", q.text] }, i))), hidden > 0 ? (_jsx(Text, { dimColor: true, children: ` …and ${hidden} more` })) : null, _jsx(Text, { dimColor: true, children: ` ↑ to edit ${queued.length === 1 ? "it" : "them"}` })] }));
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