mindweave 3.1.0 → 3.2.1

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/App.js +121 -32
  3. package/dist/cli/App.js.map +1 -1
  4. package/dist/cli/blockSpacing.js +4 -0
  5. package/dist/cli/blockSpacing.js.map +1 -1
  6. package/dist/cli/components/BlockView.js +6 -6
  7. package/dist/cli/components/BlockView.js.map +1 -1
  8. package/dist/cli/components/CommandGroup.js +100 -0
  9. package/dist/cli/components/CommandGroup.js.map +1 -0
  10. package/dist/cli/components/TipLine.js +1 -0
  11. package/dist/cli/components/TipLine.js.map +1 -1
  12. package/dist/cli/components/ToolLine.js +69 -14
  13. package/dist/cli/components/ToolLine.js.map +1 -1
  14. package/dist/cli/components/WorkGroup.js +121 -0
  15. package/dist/cli/components/WorkGroup.js.map +1 -0
  16. package/dist/cli/diffWrap.js +24 -0
  17. package/dist/cli/diffWrap.js.map +1 -0
  18. package/dist/cli/expandHits.js +74 -0
  19. package/dist/cli/expandHits.js.map +1 -0
  20. package/dist/cli/help.js +13 -1
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/mouse.js +16 -8
  23. package/dist/cli/mouse.js.map +1 -1
  24. package/dist/cli/scrollGlide.js +72 -0
  25. package/dist/cli/scrollGlide.js.map +1 -0
  26. package/dist/cli/scrollPill.js +21 -1
  27. package/dist/cli/scrollPill.js.map +1 -1
  28. package/dist/cli/selfUpdate.js +7 -0
  29. package/dist/cli/selfUpdate.js.map +1 -1
  30. package/dist/cli/shellNotes.js +21 -0
  31. package/dist/cli/shellNotes.js.map +1 -0
  32. package/dist/cli/terminalRestore.js +1 -1
  33. package/dist/cli/terminalRestore.js.map +1 -1
  34. package/dist/cli/toolDisplay.js +14 -10
  35. package/dist/cli/toolDisplay.js.map +1 -1
  36. package/dist/cli/transcript.js +100 -29
  37. package/dist/cli/transcript.js.map +1 -1
  38. package/dist/cli/uiLines.js +60 -0
  39. package/dist/cli/uiLines.js.map +1 -0
  40. package/dist/cli/wheelStep.js +29 -0
  41. package/dist/cli/wheelStep.js.map +1 -0
  42. package/dist/cli/workLines.js +84 -0
  43. package/dist/cli/workLines.js.map +1 -0
  44. package/dist/core/turnRunner.js +8 -6
  45. package/dist/core/turnRunner.js.map +1 -1
  46. package/dist/drivers/anthropic/manifest.js +23 -1
  47. package/dist/drivers/anthropic/manifest.js.map +1 -1
  48. package/dist/dynamo/engine.js +60 -32
  49. package/dist/dynamo/engine.js.map +1 -1
  50. package/dist/memory/compaction.js +24 -38
  51. package/dist/memory/compaction.js.map +1 -1
  52. package/dist/memory/earlier.js +13 -0
  53. package/dist/memory/earlier.js.map +1 -1
  54. package/dist/memory/session.js +7 -0
  55. package/dist/memory/session.js.map +1 -1
  56. package/dist/memory/sessionMemory.js +202 -72
  57. package/dist/memory/sessionMemory.js.map +1 -1
  58. package/dist/memory/store.js +11 -0
  59. package/dist/memory/store.js.map +1 -1
  60. package/dist/memory/types.js.map +1 -1
  61. package/dist/tools/detail.js +113 -1
  62. package/dist/tools/detail.js.map +1 -1
  63. package/dist/tools/readFile.js +9 -1
  64. package/dist/tools/readFile.js.map +1 -1
  65. package/dist/tools/rebuildDetail.js +155 -0
  66. package/dist/tools/rebuildDetail.js.map +1 -0
  67. package/dist/tools/runCommand.js +3 -3
  68. package/dist/tools/runCommand.js.map +1 -1
  69. package/dist/tools/sessionTools.js +24 -1
  70. package/dist/tools/sessionTools.js.map +1 -1
  71. package/dist/tools/ui.js +54 -1
  72. package/dist/tools/ui.js.map +1 -1
  73. package/package.json +3 -3
package/README.md CHANGED
@@ -50,7 +50,7 @@ npm link
50
50
 
51
51
  ## What it does
52
52
 
53
- - **16 providers, 62 models** — DeepSeek, Anthropic, OpenAI, Gemini, xAI, Mistral, Groq, Cerebras,
53
+ - **16 providers, 63 models** — DeepSeek, Anthropic, OpenAI, Gemini, xAI, Mistral, Groq, Cerebras,
54
54
  Qwen, Kimi, GLM, Meta, MiniMax, Tencent, OpenRouter, and local models through Ollama
55
55
  with no key at all. [PROVIDERS.md](src/drivers/PROVIDERS.md)
56
56
  - **Real tools** — file reads and edits, search, a shell with background jobs,
package/dist/cli/App.js CHANGED
@@ -70,12 +70,17 @@ import { PromptInput } from "./components/PromptInput.js";
70
70
  import { Picker } from "./components/Picker.js";
71
71
  import { ApprovalBox } from "./components/ApprovalBox.js";
72
72
  import { BlockView } from "./components/BlockView.js";
73
- import { initialState, reduce, trimNarration } from "./transcript.js";
73
+ import { initialState, lastExpandable, reduce, trimNarration } from "./transcript.js";
74
74
  import { isTight } from "./blockSpacing.js";
75
75
  import { parseScreenArg, screenChoices, screenNotice, startupMode } from "./screenMode.js";
76
76
  import { applyScreenMode } from "./screenShell.js";
77
77
  import { saveScreenMode } from "./screenStore.js";
78
78
  import { needsMeasure, pruneHeights } from "./blockHeights.js";
79
+ import { expandableAt, hitBlock, hitItem } from "./expandHits.js";
80
+ import { shellNote } from "./shellNotes.js";
81
+ import { readFileSync, statSync } from "node:fs";
82
+ import { compactUiDetail, rebuildFull, writtenFromDisk } from "../tools/rebuildDetail.js";
83
+ import { shownText } from "../memory/compaction.js";
79
84
  import { BASE_COMMANDS } from "./commands.js";
80
85
  import { attachmentBudget, dismissMarathon } from "../core/turnRunner.js";
81
86
  import { describeMarathonEvent, resumeMarathon, startMarathon } from "../dynamo/marathon.js";
@@ -84,7 +89,7 @@ import { isFinished, isLive, marathonUiReduce } from "./marathonUi.js";
84
89
  import { manualCommand, refusalReason } from "./selfUpdate.js";
85
90
  import { currentInstall, requestRestart, runUpdate } from "./updateRunner.js";
86
91
  import { enableMouse, readMouse, readWheel } from "./mouse.js";
87
- import { wheelLines, wheelStart } from "./wheelAccel.js";
92
+ import { wheelLines } from "./wheelStep.js";
88
93
  import { applySelection, ctrlCShouldCopy, isEmpty, selectionText } from "./selection.js";
89
94
  import { latestScreen, repaintOverlay, setFrameOverlay } from "./framebuffer/overlay.js";
90
95
  import { requestFullRepaint } from "./framebuffer/writer.js";
@@ -322,6 +327,14 @@ export function App({ resumeSessionId, initialScreen }) {
322
327
  /** The chip's cells in SCREEN coordinates, for the pointer handler. Null when there is
323
328
  * nothing to click. Published by the render, read between frames. */
324
329
  const pillHit = useRef(null);
330
+ // Where the layout's first row sits on the screen, published each render for the pointer.
331
+ const frameTopRef = useRef(0);
332
+ // The row the pointer is over, if it can be pressed. State rather than a ref: it is what
333
+ // the row draws from. Only set when it CHANGES, so a pointer crossing the screen is not
334
+ // a redraw per cell.
335
+ const [hoverId, setHoverId] = useState(null);
336
+ // Set when a row is opened or folded, read once by the height effect.
337
+ const rowToggled = useRef(false);
325
338
  const [footerHeight, setFooterHeight] = useState(0);
326
339
  // The chat viewport's REAL height. Yoga decides it now (flexGrow beside a
327
340
  // flexShrink:0 footer); this is read back purely so the scroll maths knows how
@@ -616,6 +629,9 @@ export function App({ resumeSessionId, initialScreen }) {
616
629
  const withEarlier = (session) => [...(session.earlier ?? []), ...session.transcript];
617
630
  function showResumed(transcript) {
618
631
  const readThisTurn = new Set(); // for narrationShown, as live
632
+ // Each call's name and arguments by id, so a result from a session saved before the uncut
633
+ // text was kept can have it put back together (see rebuildDetail.ts).
634
+ const callsById = new Map();
619
635
  for (const e of transcript) {
620
636
  if (e.role === "user") {
621
637
  if (!e.synthetic)
@@ -638,11 +654,13 @@ export function App({ resumeSessionId, initialScreen }) {
638
654
  const calls = (e.toolCalls ?? []).map((c) => ({ name: c.name, args: parseToolArgs(c.arguments) }));
639
655
  const shown = calls.length === 0 || narrationShown(calls, readThisTurn);
640
656
  noteReads(calls, readThisTurn);
641
- if (shown && e.content.trim()) {
657
+ const said = shownText(e);
658
+ if (shown && said && said.trim()) {
642
659
  const intermediate = calls.length > 0;
643
- dispatch({ type: "say", text: intermediate ? trimNarration(e.content) : e.content });
660
+ dispatch({ type: "say", text: intermediate ? trimNarration(said) : said });
644
661
  }
645
662
  for (const call of e.toolCalls ?? []) {
663
+ callsById.set(call.id, { name: call.name, args: parseToolArgs(call.arguments) });
646
664
  // The spawn itself is drawn by its sub-agent block live, never as a raw row.
647
665
  // Replaying it as one puts a `● SpawnSubagent(...)` in a resumed transcript
648
666
  // that was not in the live one.
@@ -669,6 +687,28 @@ export function App({ resumeSessionId, initialScreen }) {
669
687
  ok: e.isError === undefined ? !e.content.startsWith("Error:") : !e.isError,
670
688
  summary: e.summary,
671
689
  detail: e.detail,
690
+ ...(() => {
691
+ const call = callsById.get(e.toolCallId);
692
+ // A UI test saved before it became a short row comes back as the short row.
693
+ if (call?.name === "ui" && !e.detailFull) {
694
+ const compact = compactUiDetail(e.detail, e.isError);
695
+ if (compact)
696
+ return { detail: compact.detail, ...(compact.detailFull ? { detailFull: compact.detailFull } : {}) };
697
+ }
698
+ let full = e.detailFull ?? rebuildFull(call?.name, call?.args, e);
699
+ // A write whose text was cleared to save context has only the file left to open.
700
+ if (!full && call?.name === "write_file" && typeof call.args?.path === "string") {
701
+ try {
702
+ const abs = resolve(session.current?.cwd ?? process.cwd(), call.args.path);
703
+ if (statSync(abs).size < 2_000_000)
704
+ full = writtenFromDisk(e.detail, readFileSync(abs, "utf8"));
705
+ }
706
+ catch {
707
+ // gone or unreadable: the row stays as it was saved
708
+ }
709
+ }
710
+ return full ? { detailFull: full } : {};
711
+ })(),
672
712
  ...(e.detailKind ? { detailKind: e.detailKind } : {}),
673
713
  ...(e.quiet ? { quiet: true } : {}),
674
714
  ...(e.displayName ? { name: e.displayName } : {}),
@@ -701,24 +741,10 @@ export function App({ resumeSessionId, initialScreen }) {
701
741
  setBgTick((t) => t + 1);
702
742
  const mgr = session.current?.toolContext.backgroundShells;
703
743
  for (const { info: sh, kind } of mgr?.takeUiEvents() ?? []) {
704
- // A shell reaching "ready" says nothing the tool row did not already say when it
705
- // backgrounded the command, and being killed at the user's request is not news
706
- // either — they asked for it. A command that DIED ON ITS OWN is news, because
707
- // nothing else on screen would tell them their dev server had fallen over.
708
- if (kind === "ready")
709
- continue;
710
- // The watchdog flagged a running shell as stuck — a prompt it is blocked on, or a
711
- // long silence on a command that should be working. Worth a line: otherwise it sits
712
- // invisible until it times out.
713
- if (kind === "stalled") {
714
- const why = sh.stallReason === "prompt" ? "waiting for input?" : "no output for a while — stuck?";
715
- addTool(`shell #${sh.id} (${clipCmd(sh.command)}) ${why}`, { error: true });
716
- continue;
717
- }
718
- if (sh.status === "killed" && sh.stoppedBy === "user")
719
- continue;
720
- const verb = sh.status === "killed" ? "killed" : `finished — exit ${sh.exitCode}`;
721
- addTool(`shell #${sh.id} (${clipCmd(sh.command)}) ${verb}`, { error: sh.status !== "killed" && sh.exitCode !== 0 });
744
+ // Only what nothing else on screen says: see shellNotes.
745
+ const line = shellNote(sh, kind, clipCmd);
746
+ if (line)
747
+ addTool(line.text, { error: line.error });
722
748
  }
723
749
  }
724
750
  // A server's state moved. Repaint, and surface anything the pool needs the user to
@@ -1086,8 +1112,6 @@ export function App({ resumeSessionId, initialScreen }) {
1086
1112
  // counter kept climbing — and every one of those phantom lines then had to be
1087
1113
  // scrolled back down before the view moved at all. A flick or two past the top bought
1088
1114
  // a second of a wheel that did nothing, which reads as the app having frozen.
1089
- /** The wheel gesture in progress, for scroll acceleration (wheelAccel.ts). */
1090
- const wheelRef = useRef(wheelStart());
1091
1115
  const scrollBy = useCallback((lines) => {
1092
1116
  // Repaint the whole next frame. A scroll can move a row out from under the
1093
1117
  // framebuffer's model (see requestFullRepaint), which is what left a transcript row
@@ -1150,6 +1174,17 @@ export function App({ resumeSessionId, initialScreen }) {
1150
1174
  requestFullRepaint();
1151
1175
  setScrollUp(0);
1152
1176
  }
1177
+ // The keyboard way to the same thing a click does, for terminals that do not send
1178
+ // the mouse. It acts on the newest row that has more to show.
1179
+ else if (key.ctrl && _input === "o") {
1180
+ if (shell === "inline" && !reading) {
1181
+ note("Scroll up first (PageUp or the wheel), then press ctrl+O to open the newest long output.");
1182
+ return;
1183
+ }
1184
+ const target = lastExpandable(stateRef.current);
1185
+ if (target !== null)
1186
+ toggleRow(target);
1187
+ }
1153
1188
  else if (key.home && key.ctrl) {
1154
1189
  requestFullRepaint();
1155
1190
  setScrollUp(maxScrollRef.current);
@@ -1256,7 +1291,18 @@ export function App({ resumeSessionId, initialScreen }) {
1256
1291
  // Ordinary growth, not a resize — see growScroll for why this has to hold the
1257
1292
  // reader's absolute position rather than leave `scrollUp` where it was.
1258
1293
  const prev = prevContentHeight.current;
1259
- const next = growScroll(scrollUp, prev, height);
1294
+ let next = growScroll(scrollUp, prev, height);
1295
+ // A row was just opened or folded. The row the reader pressed stays where it is and
1296
+ // the change happens below it: opening from the bottom would otherwise follow the
1297
+ // new end of the output and carry the row's own first line off the top, and folding
1298
+ // would slide the view up by what was taken away.
1299
+ if (rowToggled.current && height !== prev) {
1300
+ if (height > prev && scrollUp === 0)
1301
+ next = height - prev;
1302
+ else if (height < prev)
1303
+ next = Math.max(0, scrollUp - (prev - height));
1304
+ }
1305
+ rowToggled.current = false;
1260
1306
  if (next !== scrollUp)
1261
1307
  setScrollUp(next);
1262
1308
  }
@@ -1412,12 +1458,11 @@ export function App({ resumeSessionId, initialScreen }) {
1412
1458
  // Content moves out from under a selection when the view scrolls, so the
1413
1459
  // highlight would be sitting on text that is no longer the text it copied.
1414
1460
  clearSelection();
1415
- // Signed notch count, then the distance for it: the faster the wheel is turning,
1416
- // the further each notch goes (wheelAccel.ts). A slow notch is still three lines.
1461
+ // Signed notch count, then the distance for it. The same distance for every notch,
1462
+ // applied at once (wheelStep.ts): how far the view goes follows how far the wheel
1463
+ // was turned, and nothing else.
1417
1464
  const net = notches.reduce((n, dir) => n + (dir === "up" ? 1 : -1), 0);
1418
- const step = wheelLines(wheelRef.current, net, performance.now());
1419
- wheelRef.current = step.state;
1420
- const lines = step.lines;
1465
+ const lines = wheelLines(net);
1421
1466
  // The wheel is how anyone actually scrolls, so in the inline shell it is what
1422
1467
  // opens the reading view. Turning it UP is the gesture: the reader is going back
1423
1468
  // through the conversation and wants the prompt to stay where they can type into
@@ -1437,7 +1482,13 @@ export function App({ resumeSessionId, initialScreen }) {
1437
1482
  // in a chunk is on screen at the end of it, so the ones before it are painted for
1438
1483
  // nobody. Cleared by press and release, which do their own painting.
1439
1484
  let pendingDrag = false;
1485
+ let lastMove = null;
1440
1486
  for (const event of events) {
1487
+ if (event.kind === "move") {
1488
+ // Only the last position in a chunk matters; a burst is one lookup.
1489
+ lastMove = event;
1490
+ continue;
1491
+ }
1441
1492
  if (event.kind === "press") {
1442
1493
  pendingDrag = false;
1443
1494
  // The chip is a button. It is the only thing on screen that says what it does,
@@ -1478,6 +1529,14 @@ export function App({ resumeSessionId, initialScreen }) {
1478
1529
  selection.current = null;
1479
1530
  repaintOverlay();
1480
1531
  setFrameOverlay(null);
1532
+ // A click on a row that has more to show opens it, and a second click folds
1533
+ // it. Tested BEFORE the caret, because a click on the transcript is never on
1534
+ // the input anyway, and a click that does nothing is the worse outcome.
1535
+ const hitId = expandableAt(event.y, frameTopRef.current, chatRef.current);
1536
+ if (hitId !== null) {
1537
+ toggleRow(hitId);
1538
+ continue;
1539
+ }
1481
1540
  placeCaretAt(event.x, event.y);
1482
1541
  continue;
1483
1542
  }
@@ -1495,11 +1554,30 @@ export function App({ resumeSessionId, initialScreen }) {
1495
1554
  }
1496
1555
  if (pendingDrag)
1497
1556
  repaintOverlay();
1557
+ // The pointer, resting or moving: light the row under it if it can be pressed, and put
1558
+ // the light out when it leaves. A press or a wheel turn clears it too, below, because
1559
+ // the content moves out from under a pointer that did not.
1560
+ if (lastMove) {
1561
+ const over = expandableAt(lastMove.y, frameTopRef.current, chatRef.current);
1562
+ setHoverId((h) => (h === over ? h : over));
1563
+ }
1564
+ else if (notches.length > 0 || events.some((e) => e.kind === "press")) {
1565
+ setHoverId((h) => (h === null ? h : null));
1566
+ }
1498
1567
  // A real keystroke, so put any highlight away — the same as a terminal's own
1499
1568
  // selection does the moment you type.
1500
1569
  if (events.length === 0 && notches.length === 0)
1501
1570
  clearSelection();
1502
1571
  }, { isActive: true });
1572
+ /** Open or fold a row, keeping it where the reader pressed it. See the height effect. */
1573
+ function toggleRow(hit) {
1574
+ rowToggled.current = true;
1575
+ // A hit on one command inside a commands row opens that command; anything else is a row.
1576
+ if (hitItem(hit) >= 0)
1577
+ dispatch({ type: "toggleItem", id: hitBlock(hit), index: hitItem(hit) });
1578
+ else
1579
+ dispatch({ type: "toggleExpand", id: hit });
1580
+ }
1503
1581
  function endTurn() {
1504
1582
  releaseLiveGates();
1505
1583
  if (turnStart.current != null)
@@ -1957,6 +2035,7 @@ export function App({ resumeSessionId, initialScreen }) {
1957
2035
  summary: e.summary,
1958
2036
  detail: e.detail,
1959
2037
  detailKind: e.detailKind,
2038
+ detailFull: e.detailFull,
1960
2039
  quiet: e.quiet,
1961
2040
  action: e.displayKind,
1962
2041
  name: e.displayName,
@@ -2536,6 +2615,14 @@ export function App({ resumeSessionId, initialScreen }) {
2536
2615
  return;
2537
2616
  }
2538
2617
  const install = currentInstall();
2618
+ if (install.kind === "app") {
2619
+ const latest = await checkForUpdate({ readCacheImpl: () => null });
2620
+ const current = appVersion();
2621
+ note(latest
2622
+ ? `${refusalReason(install)} v${latest} is out (this is v${current}); update the app from Settings, About.`
2623
+ : `Already on the latest${current ? ` (v${current})` : ""}. ${refusalReason(install)}`);
2624
+ return;
2625
+ }
2539
2626
  if (install.kind !== "global") {
2540
2627
  // Never guessed at. Overwriting a working tree or another project's dependency
2541
2628
  // is the one thing this command must not do; see selfUpdate.ts.
@@ -3570,6 +3657,7 @@ export function App({ resumeSessionId, initialScreen }) {
3570
3657
  newReplies: countNewReplies(allBlocks, scrollMark.current),
3571
3658
  overlayOpen: overlay !== null,
3572
3659
  width,
3660
+ maxScroll,
3573
3661
  });
3574
3662
  /**
3575
3663
  * The chip's cells in SCREEN coordinates, so a click can be tested against them.
@@ -3586,6 +3674,7 @@ export function App({ resumeSessionId, initialScreen }) {
3586
3674
  * measure nothing for itself.
3587
3675
  */
3588
3676
  const frameTop = readingInline ? Math.max(0, rows - frameHeight) : 0;
3677
+ frameTopRef.current = frameTop;
3589
3678
  pillHit.current = pill !== null && pillRow.current !== null ? pillBounds(pill, width, frameTop + pillRow.current) : null;
3590
3679
  // The whole conversation stays reachable: the cap is only a safety valve (see
3591
3680
  // SCROLLBACK_BLOCKS). Blocks outside the window cost one number each here and nothing
@@ -3692,13 +3781,13 @@ export function App({ resumeSessionId, initialScreen }) {
3692
3781
  _jsx(Box, { ref: (node) => {
3693
3782
  if (node && needsMeasure(blockHeights.current.get(b.id), b))
3694
3783
  toMeasure.current.set(b, node);
3695
- }, flexShrink: 0, flexDirection: "column", children: _jsx(BlockView, { block: b, columns: width, tightTop: isTight(allBlocks, offset + idx) }) }, b.id));
3784
+ }, flexShrink: 0, flexDirection: "column", children: _jsx(BlockView, { block: b, columns: width, tightTop: isTight(allBlocks, offset + idx), hovered: hoverId !== null && hitBlock(hoverId) === b.id && hitItem(hoverId) < 0, hoveredItem: hoverId !== null && hitBlock(hoverId) === b.id ? hitItem(hoverId) : -1 }) }, b.id));
3696
3785
  }), padMiddle > 0 ? _jsx(Box, { flexShrink: 0, height: padMiddle }) : null, rendered.slice(known).map((b, i) => {
3697
3786
  const idx = known + i;
3698
3787
  return (_jsx(Box, { ref: (node) => {
3699
3788
  if (node && needsMeasure(blockHeights.current.get(b.id), b))
3700
3789
  toMeasure.current.set(b, node);
3701
- }, flexShrink: 0, flexDirection: "column", children: _jsx(BlockView, { block: b, columns: width, tightTop: isTight(allBlocks, offset + idx) }) }, b.id));
3790
+ }, flexShrink: 0, flexDirection: "column", children: _jsx(BlockView, { block: b, columns: width, tightTop: isTight(allBlocks, offset + idx), hovered: hoverId !== null && hitBlock(hoverId) === b.id && hitItem(hoverId) < 0, hoveredItem: hoverId !== null && hitBlock(hoverId) === b.id ? hitItem(hoverId) : -1 }) }, b.id));
3702
3791
  })] }) }), pill ? (_jsx(Box
3703
3792
  // Measured so a CLICK can find it. The row is the only part of the chip's
3704
3793
  // position the layout owns — the columns fall out of centring, which