mindweave 3.0.1 → 3.2.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 (177) hide show
  1. package/README.md +36 -4
  2. package/dist/alternator/chassis/lsp.js +36 -4
  3. package/dist/alternator/chassis/lsp.js.map +1 -1
  4. package/dist/alternator/chassis/provision.js +54 -6
  5. package/dist/alternator/chassis/provision.js.map +1 -1
  6. package/dist/alternator/chassis/servers.js +34 -21
  7. package/dist/alternator/chassis/servers.js.map +1 -1
  8. package/dist/cli/App.js +118 -16
  9. package/dist/cli/App.js.map +1 -1
  10. package/dist/cli/attachments.js +37 -1
  11. package/dist/cli/attachments.js.map +1 -1
  12. package/dist/cli/bootstrap.js +25 -3
  13. package/dist/cli/bootstrap.js.map +1 -1
  14. package/dist/cli/components/BlockView.js +5 -5
  15. package/dist/cli/components/BlockView.js.map +1 -1
  16. package/dist/cli/components/CommandGroup.js +100 -0
  17. package/dist/cli/components/CommandGroup.js.map +1 -0
  18. package/dist/cli/components/MarathonBox.js +2 -1
  19. package/dist/cli/components/MarathonBox.js.map +1 -1
  20. package/dist/cli/components/TipLine.js +1 -0
  21. package/dist/cli/components/TipLine.js.map +1 -1
  22. package/dist/cli/components/ToolLine.js +69 -14
  23. package/dist/cli/components/ToolLine.js.map +1 -1
  24. package/dist/cli/components/WorkGroup.js +115 -0
  25. package/dist/cli/components/WorkGroup.js.map +1 -0
  26. package/dist/cli/diffWrap.js +24 -0
  27. package/dist/cli/diffWrap.js.map +1 -0
  28. package/dist/cli/expandHits.js +74 -0
  29. package/dist/cli/expandHits.js.map +1 -0
  30. package/dist/cli/marathonUi.js +2 -0
  31. package/dist/cli/marathonUi.js.map +1 -1
  32. package/dist/cli/mouse.js +16 -8
  33. package/dist/cli/mouse.js.map +1 -1
  34. package/dist/cli/scrollGlide.js +72 -0
  35. package/dist/cli/scrollGlide.js.map +1 -0
  36. package/dist/cli/scrollPill.js +21 -1
  37. package/dist/cli/scrollPill.js.map +1 -1
  38. package/dist/cli/selfUpdate.js +7 -0
  39. package/dist/cli/selfUpdate.js.map +1 -1
  40. package/dist/cli/terminalRestore.js +1 -1
  41. package/dist/cli/terminalRestore.js.map +1 -1
  42. package/dist/cli/toolDisplay.js +14 -10
  43. package/dist/cli/toolDisplay.js.map +1 -1
  44. package/dist/cli/transcript.js +100 -29
  45. package/dist/cli/transcript.js.map +1 -1
  46. package/dist/cli/uiLines.js +60 -0
  47. package/dist/cli/uiLines.js.map +1 -0
  48. package/dist/cli/wheelStep.js +29 -0
  49. package/dist/cli/wheelStep.js.map +1 -0
  50. package/dist/cli/workLines.js +84 -0
  51. package/dist/cli/workLines.js.map +1 -0
  52. package/dist/core/permissions.js +26 -1
  53. package/dist/core/permissions.js.map +1 -1
  54. package/dist/core/spendView.js +8 -1
  55. package/dist/core/spendView.js.map +1 -1
  56. package/dist/core/turnRunner.js +36 -8
  57. package/dist/core/turnRunner.js.map +1 -1
  58. package/dist/drivers/deepseek/inlineTools.js +36 -2
  59. package/dist/drivers/deepseek/inlineTools.js.map +1 -1
  60. package/dist/drivers/ollama/endpoint.js +25 -0
  61. package/dist/drivers/ollama/endpoint.js.map +1 -1
  62. package/dist/dynamo/engine.js +414 -88
  63. package/dist/dynamo/engine.js.map +1 -1
  64. package/dist/dynamo/guard.js +11 -4
  65. package/dist/dynamo/guard.js.map +1 -1
  66. package/dist/dynamo/hooks.js +209 -0
  67. package/dist/dynamo/hooks.js.map +1 -0
  68. package/dist/dynamo/marathon.js +67 -10
  69. package/dist/dynamo/marathon.js.map +1 -1
  70. package/dist/dynamo/todoReminder.js +26 -0
  71. package/dist/dynamo/todoReminder.js.map +1 -0
  72. package/dist/governor/forbidden.js +19 -0
  73. package/dist/governor/forbidden.js.map +1 -1
  74. package/dist/governor/freshness.js +1 -1
  75. package/dist/governor/freshness.js.map +1 -1
  76. package/dist/governor/index.js +8 -1
  77. package/dist/governor/index.js.map +1 -1
  78. package/dist/governor/rules.js +1 -0
  79. package/dist/governor/rules.js.map +1 -1
  80. package/dist/governor/write.js +50 -2
  81. package/dist/governor/write.js.map +1 -1
  82. package/dist/mcp/config.js +0 -41
  83. package/dist/mcp/config.js.map +1 -1
  84. package/dist/mcp/projectApproval.js +117 -0
  85. package/dist/mcp/projectApproval.js.map +1 -0
  86. package/dist/mcp/resultStore.js +19 -5
  87. package/dist/mcp/resultStore.js.map +1 -1
  88. package/dist/mcp/transport/stdio.js +4 -1
  89. package/dist/mcp/transport/stdio.js.map +1 -1
  90. package/dist/memory/allMetas.js +1 -1
  91. package/dist/memory/allMetas.js.map +1 -1
  92. package/dist/memory/autoMemory.js +1 -0
  93. package/dist/memory/autoMemory.js.map +1 -1
  94. package/dist/memory/clearedArchive.js +120 -0
  95. package/dist/memory/clearedArchive.js.map +1 -0
  96. package/dist/memory/compaction.js +178 -15
  97. package/dist/memory/compaction.js.map +1 -1
  98. package/dist/memory/counters.js +64 -0
  99. package/dist/memory/counters.js.map +1 -0
  100. package/dist/memory/projectNotes.js +61 -13
  101. package/dist/memory/projectNotes.js.map +1 -1
  102. package/dist/memory/rewind.js +6 -2
  103. package/dist/memory/rewind.js.map +1 -1
  104. package/dist/memory/session.js +13 -5
  105. package/dist/memory/session.js.map +1 -1
  106. package/dist/memory/sessionMemory.js +6 -2
  107. package/dist/memory/sessionMemory.js.map +1 -1
  108. package/dist/memory/store.js +3 -0
  109. package/dist/memory/store.js.map +1 -1
  110. package/dist/memory/types.js.map +1 -1
  111. package/dist/memory/workingSet.js +0 -375
  112. package/dist/memory/workingSet.js.map +1 -1
  113. package/dist/project/context.js +54 -7
  114. package/dist/project/context.js.map +1 -1
  115. package/dist/tools/approval.js +19 -4
  116. package/dist/tools/approval.js.map +1 -1
  117. package/dist/tools/backgroundShells.js +8 -1
  118. package/dist/tools/backgroundShells.js.map +1 -1
  119. package/dist/tools/browser.js +2 -1
  120. package/dist/tools/browser.js.map +1 -1
  121. package/dist/tools/checkpoints.js +28 -4
  122. package/dist/tools/checkpoints.js.map +1 -1
  123. package/dist/tools/childEnv.js +50 -0
  124. package/dist/tools/childEnv.js.map +1 -0
  125. package/dist/tools/codeIntel.js +4 -1
  126. package/dist/tools/codeIntel.js.map +1 -1
  127. package/dist/tools/commandParse.js +238 -0
  128. package/dist/tools/commandParse.js.map +1 -0
  129. package/dist/tools/commandPolicy.js +283 -0
  130. package/dist/tools/commandPolicy.js.map +1 -0
  131. package/dist/tools/detail.js +113 -1
  132. package/dist/tools/detail.js.map +1 -1
  133. package/dist/tools/edit.js +2 -1
  134. package/dist/tools/edit.js.map +1 -1
  135. package/dist/tools/editRipple.js +18 -3
  136. package/dist/tools/editRipple.js.map +1 -1
  137. package/dist/tools/editTarget.js +11 -6
  138. package/dist/tools/editTarget.js.map +1 -1
  139. package/dist/tools/governorTools.js +61 -1
  140. package/dist/tools/governorTools.js.map +1 -1
  141. package/dist/tools/grep.js +13 -5
  142. package/dist/tools/grep.js.map +1 -1
  143. package/dist/tools/guard.js +249 -2
  144. package/dist/tools/guard.js.map +1 -1
  145. package/dist/tools/invisible.js +62 -0
  146. package/dist/tools/invisible.js.map +1 -0
  147. package/dist/tools/killTree.js +3 -0
  148. package/dist/tools/killTree.js.map +1 -1
  149. package/dist/tools/readFile.js +11 -3
  150. package/dist/tools/readFile.js.map +1 -1
  151. package/dist/tools/readSymbol.js +5 -24
  152. package/dist/tools/readSymbol.js.map +1 -1
  153. package/dist/tools/rebuildDetail.js +155 -0
  154. package/dist/tools/rebuildDetail.js.map +1 -0
  155. package/dist/tools/replaceSymbol.js +1 -1
  156. package/dist/tools/replaceSymbol.js.map +1 -1
  157. package/dist/tools/ripgrepShell.js +56 -0
  158. package/dist/tools/ripgrepShell.js.map +1 -0
  159. package/dist/tools/runCommand.js +77 -18
  160. package/dist/tools/runCommand.js.map +1 -1
  161. package/dist/tools/runsLater.js +50 -0
  162. package/dist/tools/runsLater.js.map +1 -0
  163. package/dist/tools/serverConsent.js +27 -0
  164. package/dist/tools/serverConsent.js.map +1 -0
  165. package/dist/tools/ui.js +81 -1
  166. package/dist/tools/ui.js.map +1 -1
  167. package/dist/tools/untrusted.js +22 -1
  168. package/dist/tools/untrusted.js.map +1 -1
  169. package/dist/tools/viewImage.js +2 -2
  170. package/dist/tools/viewImage.js.map +1 -1
  171. package/dist/tools/webFetch.js +76 -8
  172. package/dist/tools/webFetch.js.map +1 -1
  173. package/dist/tools/workspace.js +87 -24
  174. package/dist/tools/workspace.js.map +1 -1
  175. package/dist/tools/writeFile.js +9 -4
  176. package/dist/tools/writeFile.js.map +1 -1
  177. package/package.json +4 -4
package/dist/cli/App.js CHANGED
@@ -70,21 +70,25 @@ 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 { readFileSync, statSync } from "node:fs";
81
+ import { compactUiDetail, rebuildFull, writtenFromDisk } from "../tools/rebuildDetail.js";
82
+ import { shownText } from "../memory/compaction.js";
79
83
  import { BASE_COMMANDS } from "./commands.js";
80
- import { dismissMarathon } from "../core/turnRunner.js";
84
+ import { attachmentBudget, dismissMarathon } from "../core/turnRunner.js";
81
85
  import { describeMarathonEvent, resumeMarathon, startMarathon } from "../dynamo/marathon.js";
82
86
  import { MarathonBox } from "./components/MarathonBox.js";
83
87
  import { isFinished, isLive, marathonUiReduce } from "./marathonUi.js";
84
88
  import { manualCommand, refusalReason } from "./selfUpdate.js";
85
89
  import { currentInstall, requestRestart, runUpdate } from "./updateRunner.js";
86
90
  import { enableMouse, readMouse, readWheel } from "./mouse.js";
87
- import { wheelLines, wheelStart } from "./wheelAccel.js";
91
+ import { wheelLines } from "./wheelStep.js";
88
92
  import { applySelection, ctrlCShouldCopy, isEmpty, selectionText } from "./selection.js";
89
93
  import { latestScreen, repaintOverlay, setFrameOverlay } from "./framebuffer/overlay.js";
90
94
  import { requestFullRepaint } from "./framebuffer/writer.js";
@@ -322,6 +326,14 @@ export function App({ resumeSessionId, initialScreen }) {
322
326
  /** The chip's cells in SCREEN coordinates, for the pointer handler. Null when there is
323
327
  * nothing to click. Published by the render, read between frames. */
324
328
  const pillHit = useRef(null);
329
+ // Where the layout's first row sits on the screen, published each render for the pointer.
330
+ const frameTopRef = useRef(0);
331
+ // The row the pointer is over, if it can be pressed. State rather than a ref: it is what
332
+ // the row draws from. Only set when it CHANGES, so a pointer crossing the screen is not
333
+ // a redraw per cell.
334
+ const [hoverId, setHoverId] = useState(null);
335
+ // Set when a row is opened or folded, read once by the height effect.
336
+ const rowToggled = useRef(false);
325
337
  const [footerHeight, setFooterHeight] = useState(0);
326
338
  // The chat viewport's REAL height. Yoga decides it now (flexGrow beside a
327
339
  // flexShrink:0 footer); this is read back purely so the scroll maths knows how
@@ -616,6 +628,9 @@ export function App({ resumeSessionId, initialScreen }) {
616
628
  const withEarlier = (session) => [...(session.earlier ?? []), ...session.transcript];
617
629
  function showResumed(transcript) {
618
630
  const readThisTurn = new Set(); // for narrationShown, as live
631
+ // Each call's name and arguments by id, so a result from a session saved before the uncut
632
+ // text was kept can have it put back together (see rebuildDetail.ts).
633
+ const callsById = new Map();
619
634
  for (const e of transcript) {
620
635
  if (e.role === "user") {
621
636
  if (!e.synthetic)
@@ -638,11 +653,13 @@ export function App({ resumeSessionId, initialScreen }) {
638
653
  const calls = (e.toolCalls ?? []).map((c) => ({ name: c.name, args: parseToolArgs(c.arguments) }));
639
654
  const shown = calls.length === 0 || narrationShown(calls, readThisTurn);
640
655
  noteReads(calls, readThisTurn);
641
- if (shown && e.content.trim()) {
656
+ const said = shownText(e);
657
+ if (shown && said && said.trim()) {
642
658
  const intermediate = calls.length > 0;
643
- dispatch({ type: "say", text: intermediate ? trimNarration(e.content) : e.content });
659
+ dispatch({ type: "say", text: intermediate ? trimNarration(said) : said });
644
660
  }
645
661
  for (const call of e.toolCalls ?? []) {
662
+ callsById.set(call.id, { name: call.name, args: parseToolArgs(call.arguments) });
646
663
  // The spawn itself is drawn by its sub-agent block live, never as a raw row.
647
664
  // Replaying it as one puts a `● SpawnSubagent(...)` in a resumed transcript
648
665
  // that was not in the live one.
@@ -669,6 +686,28 @@ export function App({ resumeSessionId, initialScreen }) {
669
686
  ok: e.isError === undefined ? !e.content.startsWith("Error:") : !e.isError,
670
687
  summary: e.summary,
671
688
  detail: e.detail,
689
+ ...(() => {
690
+ const call = callsById.get(e.toolCallId);
691
+ // A UI test saved before it became a short row comes back as the short row.
692
+ if (call?.name === "ui" && !e.detailFull) {
693
+ const compact = compactUiDetail(e.detail, e.isError);
694
+ if (compact)
695
+ return { detail: compact.detail, ...(compact.detailFull ? { detailFull: compact.detailFull } : {}) };
696
+ }
697
+ let full = e.detailFull ?? rebuildFull(call?.name, call?.args, e);
698
+ // A write whose text was cleared to save context has only the file left to open.
699
+ if (!full && call?.name === "write_file" && typeof call.args?.path === "string") {
700
+ try {
701
+ const abs = resolve(session.current?.cwd ?? process.cwd(), call.args.path);
702
+ if (statSync(abs).size < 2_000_000)
703
+ full = writtenFromDisk(e.detail, readFileSync(abs, "utf8"));
704
+ }
705
+ catch {
706
+ // gone or unreadable: the row stays as it was saved
707
+ }
708
+ }
709
+ return full ? { detailFull: full } : {};
710
+ })(),
672
711
  ...(e.detailKind ? { detailKind: e.detailKind } : {}),
673
712
  ...(e.quiet ? { quiet: true } : {}),
674
713
  ...(e.displayName ? { name: e.displayName } : {}),
@@ -1086,8 +1125,6 @@ export function App({ resumeSessionId, initialScreen }) {
1086
1125
  // counter kept climbing — and every one of those phantom lines then had to be
1087
1126
  // scrolled back down before the view moved at all. A flick or two past the top bought
1088
1127
  // 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
1128
  const scrollBy = useCallback((lines) => {
1092
1129
  // Repaint the whole next frame. A scroll can move a row out from under the
1093
1130
  // framebuffer's model (see requestFullRepaint), which is what left a transcript row
@@ -1150,6 +1187,17 @@ export function App({ resumeSessionId, initialScreen }) {
1150
1187
  requestFullRepaint();
1151
1188
  setScrollUp(0);
1152
1189
  }
1190
+ // The keyboard way to the same thing a click does, for terminals that do not send
1191
+ // the mouse. It acts on the newest row that has more to show.
1192
+ else if (key.ctrl && _input === "o") {
1193
+ if (shell === "inline" && !reading) {
1194
+ note("Scroll up first (PageUp or the wheel), then press ctrl+O to open the newest long output.");
1195
+ return;
1196
+ }
1197
+ const target = lastExpandable(stateRef.current);
1198
+ if (target !== null)
1199
+ toggleRow(target);
1200
+ }
1153
1201
  else if (key.home && key.ctrl) {
1154
1202
  requestFullRepaint();
1155
1203
  setScrollUp(maxScrollRef.current);
@@ -1256,7 +1304,18 @@ export function App({ resumeSessionId, initialScreen }) {
1256
1304
  // Ordinary growth, not a resize — see growScroll for why this has to hold the
1257
1305
  // reader's absolute position rather than leave `scrollUp` where it was.
1258
1306
  const prev = prevContentHeight.current;
1259
- const next = growScroll(scrollUp, prev, height);
1307
+ let next = growScroll(scrollUp, prev, height);
1308
+ // A row was just opened or folded. The row the reader pressed stays where it is and
1309
+ // the change happens below it: opening from the bottom would otherwise follow the
1310
+ // new end of the output and carry the row's own first line off the top, and folding
1311
+ // would slide the view up by what was taken away.
1312
+ if (rowToggled.current && height !== prev) {
1313
+ if (height > prev && scrollUp === 0)
1314
+ next = height - prev;
1315
+ else if (height < prev)
1316
+ next = Math.max(0, scrollUp - (prev - height));
1317
+ }
1318
+ rowToggled.current = false;
1260
1319
  if (next !== scrollUp)
1261
1320
  setScrollUp(next);
1262
1321
  }
@@ -1412,12 +1471,11 @@ export function App({ resumeSessionId, initialScreen }) {
1412
1471
  // Content moves out from under a selection when the view scrolls, so the
1413
1472
  // highlight would be sitting on text that is no longer the text it copied.
1414
1473
  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.
1474
+ // Signed notch count, then the distance for it. The same distance for every notch,
1475
+ // applied at once (wheelStep.ts): how far the view goes follows how far the wheel
1476
+ // was turned, and nothing else.
1417
1477
  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;
1478
+ const lines = wheelLines(net);
1421
1479
  // The wheel is how anyone actually scrolls, so in the inline shell it is what
1422
1480
  // opens the reading view. Turning it UP is the gesture: the reader is going back
1423
1481
  // through the conversation and wants the prompt to stay where they can type into
@@ -1437,7 +1495,13 @@ export function App({ resumeSessionId, initialScreen }) {
1437
1495
  // in a chunk is on screen at the end of it, so the ones before it are painted for
1438
1496
  // nobody. Cleared by press and release, which do their own painting.
1439
1497
  let pendingDrag = false;
1498
+ let lastMove = null;
1440
1499
  for (const event of events) {
1500
+ if (event.kind === "move") {
1501
+ // Only the last position in a chunk matters; a burst is one lookup.
1502
+ lastMove = event;
1503
+ continue;
1504
+ }
1441
1505
  if (event.kind === "press") {
1442
1506
  pendingDrag = false;
1443
1507
  // The chip is a button. It is the only thing on screen that says what it does,
@@ -1478,6 +1542,14 @@ export function App({ resumeSessionId, initialScreen }) {
1478
1542
  selection.current = null;
1479
1543
  repaintOverlay();
1480
1544
  setFrameOverlay(null);
1545
+ // A click on a row that has more to show opens it, and a second click folds
1546
+ // it. Tested BEFORE the caret, because a click on the transcript is never on
1547
+ // the input anyway, and a click that does nothing is the worse outcome.
1548
+ const hitId = expandableAt(event.y, frameTopRef.current, chatRef.current);
1549
+ if (hitId !== null) {
1550
+ toggleRow(hitId);
1551
+ continue;
1552
+ }
1481
1553
  placeCaretAt(event.x, event.y);
1482
1554
  continue;
1483
1555
  }
@@ -1495,11 +1567,30 @@ export function App({ resumeSessionId, initialScreen }) {
1495
1567
  }
1496
1568
  if (pendingDrag)
1497
1569
  repaintOverlay();
1570
+ // The pointer, resting or moving: light the row under it if it can be pressed, and put
1571
+ // the light out when it leaves. A press or a wheel turn clears it too, below, because
1572
+ // the content moves out from under a pointer that did not.
1573
+ if (lastMove) {
1574
+ const over = expandableAt(lastMove.y, frameTopRef.current, chatRef.current);
1575
+ setHoverId((h) => (h === over ? h : over));
1576
+ }
1577
+ else if (notches.length > 0 || events.some((e) => e.kind === "press")) {
1578
+ setHoverId((h) => (h === null ? h : null));
1579
+ }
1498
1580
  // A real keystroke, so put any highlight away — the same as a terminal's own
1499
1581
  // selection does the moment you type.
1500
1582
  if (events.length === 0 && notches.length === 0)
1501
1583
  clearSelection();
1502
1584
  }, { isActive: true });
1585
+ /** Open or fold a row, keeping it where the reader pressed it. See the height effect. */
1586
+ function toggleRow(hit) {
1587
+ rowToggled.current = true;
1588
+ // A hit on one command inside a commands row opens that command; anything else is a row.
1589
+ if (hitItem(hit) >= 0)
1590
+ dispatch({ type: "toggleItem", id: hitBlock(hit), index: hitItem(hit) });
1591
+ else
1592
+ dispatch({ type: "toggleExpand", id: hit });
1593
+ }
1503
1594
  function endTurn() {
1504
1595
  releaseLiveGates();
1505
1596
  if (turnStart.current != null)
@@ -1850,7 +1941,7 @@ export function App({ resumeSessionId, initialScreen }) {
1850
1941
  // Dropped files are carried in the buffer as short handles. Put the real paths back
1851
1942
  // before anything is resolved against the disk, and hand the same handle back as the
1852
1943
  // label so the chat shows what the user typed rather than a third name for the file.
1853
- const { modelText, displayText, notes, images } = await resolveAttachments(expandHandles(text, dropHandles.current), s.cwd, canSeeImages, (abs) => dropHandles.current.labelFor(abs));
1944
+ const { modelText, displayText, notes, images } = await resolveAttachments(expandHandles(text, dropHandles.current), s.cwd, canSeeImages, (abs) => dropHandles.current.labelFor(abs), attachmentBudget(s.modelConfig.model));
1854
1945
  // Restore any collapsed pastes into the model's copy only (the chat keeps chips).
1855
1946
  return { content: expandPastes(modelText), displayText, notes, images };
1856
1947
  }
@@ -1957,6 +2048,7 @@ export function App({ resumeSessionId, initialScreen }) {
1957
2048
  summary: e.summary,
1958
2049
  detail: e.detail,
1959
2050
  detailKind: e.detailKind,
2051
+ detailFull: e.detailFull,
1960
2052
  quiet: e.quiet,
1961
2053
  action: e.displayKind,
1962
2054
  name: e.displayName,
@@ -2536,6 +2628,14 @@ export function App({ resumeSessionId, initialScreen }) {
2536
2628
  return;
2537
2629
  }
2538
2630
  const install = currentInstall();
2631
+ if (install.kind === "app") {
2632
+ const latest = await checkForUpdate({ readCacheImpl: () => null });
2633
+ const current = appVersion();
2634
+ note(latest
2635
+ ? `${refusalReason(install)} v${latest} is out (this is v${current}); update the app from Settings, About.`
2636
+ : `Already on the latest${current ? ` (v${current})` : ""}. ${refusalReason(install)}`);
2637
+ return;
2638
+ }
2539
2639
  if (install.kind !== "global") {
2540
2640
  // Never guessed at. Overwriting a working tree or another project's dependency
2541
2641
  // is the one thing this command must not do; see selfUpdate.ts.
@@ -3570,6 +3670,7 @@ export function App({ resumeSessionId, initialScreen }) {
3570
3670
  newReplies: countNewReplies(allBlocks, scrollMark.current),
3571
3671
  overlayOpen: overlay !== null,
3572
3672
  width,
3673
+ maxScroll,
3573
3674
  });
3574
3675
  /**
3575
3676
  * The chip's cells in SCREEN coordinates, so a click can be tested against them.
@@ -3586,6 +3687,7 @@ export function App({ resumeSessionId, initialScreen }) {
3586
3687
  * measure nothing for itself.
3587
3688
  */
3588
3689
  const frameTop = readingInline ? Math.max(0, rows - frameHeight) : 0;
3690
+ frameTopRef.current = frameTop;
3589
3691
  pillHit.current = pill !== null && pillRow.current !== null ? pillBounds(pill, width, frameTop + pillRow.current) : null;
3590
3692
  // The whole conversation stays reachable: the cap is only a safety valve (see
3591
3693
  // SCROLLBACK_BLOCKS). Blocks outside the window cost one number each here and nothing
@@ -3692,13 +3794,13 @@ export function App({ resumeSessionId, initialScreen }) {
3692
3794
  _jsx(Box, { ref: (node) => {
3693
3795
  if (node && needsMeasure(blockHeights.current.get(b.id), b))
3694
3796
  toMeasure.current.set(b, node);
3695
- }, flexShrink: 0, flexDirection: "column", children: _jsx(BlockView, { block: b, columns: width, tightTop: isTight(allBlocks, offset + idx) }) }, b.id));
3797
+ }, 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
3798
  }), padMiddle > 0 ? _jsx(Box, { flexShrink: 0, height: padMiddle }) : null, rendered.slice(known).map((b, i) => {
3697
3799
  const idx = known + i;
3698
3800
  return (_jsx(Box, { ref: (node) => {
3699
3801
  if (node && needsMeasure(blockHeights.current.get(b.id), b))
3700
3802
  toMeasure.current.set(b, node);
3701
- }, flexShrink: 0, flexDirection: "column", children: _jsx(BlockView, { block: b, columns: width, tightTop: isTight(allBlocks, offset + idx) }) }, b.id));
3803
+ }, 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
3804
  })] }) }), pill ? (_jsx(Box
3703
3805
  // Measured so a CLICK can find it. The row is the only part of the chip's
3704
3806
  // position the layout owns — the columns fall out of centring, which