mindweave 3.2.7 → 3.2.8
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 +247 -21
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/blockBoxes.js +57 -0
- package/dist/cli/blockBoxes.js.map +1 -0
- package/dist/cli/chatAnchor.js +34 -0
- package/dist/cli/chatAnchor.js.map +1 -1
- package/dist/cli/commandBackground.js +16 -0
- package/dist/cli/commandBackground.js.map +1 -0
- package/dist/cli/components/PromptInput.js +16 -3
- package/dist/cli/components/PromptInput.js.map +1 -1
- package/dist/cli/components/WorkGroup.js +70 -16
- package/dist/cli/components/WorkGroup.js.map +1 -1
- package/dist/cli/expandHits.js +18 -0
- package/dist/cli/expandHits.js.map +1 -1
- package/dist/cli/framebuffer/frameHold.js +58 -0
- package/dist/cli/framebuffer/frameHold.js.map +1 -0
- package/dist/cli/framebuffer/writer.js +18 -0
- package/dist/cli/framebuffer/writer.js.map +1 -1
- package/dist/cli/help.js +8 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/selectionGrow.js +244 -0
- package/dist/cli/selectionGrow.js.map +1 -0
- package/dist/cli/toolDisplay.js +21 -3
- package/dist/cli/toolDisplay.js.map +1 -1
- package/dist/cli/workLines.js +86 -4
- package/dist/cli/workLines.js.map +1 -1
- package/dist/dynamo/engine.js +13 -3
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/dynamo/verify.js +21 -1
- package/dist/dynamo/verify.js.map +1 -1
- package/dist/mcp/connection.js +52 -9
- package/dist/mcp/connection.js.map +1 -1
- package/dist/mcp/manager.js +61 -42
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/resultView.js +116 -0
- package/dist/mcp/resultView.js.map +1 -0
- package/dist/mcp/transport/http.js +12 -4
- package/dist/mcp/transport/http.js.map +1 -1
- package/dist/mcp/transport/stdio.js +30 -5
- package/dist/mcp/transport/stdio.js.map +1 -1
- package/dist/mcp/transport/types.js +14 -0
- package/dist/mcp/transport/types.js.map +1 -1
- package/dist/memory/session.js +2 -0
- package/dist/memory/session.js.map +1 -1
- package/dist/tools/foregroundCommands.js +67 -0
- package/dist/tools/foregroundCommands.js.map +1 -0
- package/dist/tools/mcpAdd.js +124 -16
- package/dist/tools/mcpAdd.js.map +1 -1
- package/dist/tools/mindweaveStatus.js +1 -1
- package/dist/tools/mindweaveStatus.js.map +1 -1
- package/dist/tools/runCommand.js +61 -24
- package/dist/tools/runCommand.js.map +1 -1
- package/package.json +3 -3
package/dist/cli/App.js
CHANGED
|
@@ -77,8 +77,9 @@ import { parseScreenArg, screenChoices, screenNotice, startupMode } from "./scre
|
|
|
77
77
|
import { applyScreenMode } from "./screenShell.js";
|
|
78
78
|
import { saveScreenMode } from "./screenStore.js";
|
|
79
79
|
import { FULL_TAIL_BLOCKS, needsMeasure, pruneHeights, seedHeights } from "./blockHeights.js";
|
|
80
|
-
import { expandableAt, hitBlock, hitItem } from "./expandHits.js";
|
|
80
|
+
import { expandableAt, hitBlock, hitItem, revealNode } from "./expandHits.js";
|
|
81
81
|
import { shellNote } from "./shellNotes.js";
|
|
82
|
+
import { pressBackground } from "./commandBackground.js";
|
|
82
83
|
import { readFileSync, statSync } from "node:fs";
|
|
83
84
|
import { compactUiDetail, rebuildFull, writtenFromDisk } from "../tools/rebuildDetail.js";
|
|
84
85
|
import { shownText } from "../memory/compaction.js";
|
|
@@ -92,10 +93,13 @@ import { currentInstall, requestRestart, runUpdate } from "./updateRunner.js";
|
|
|
92
93
|
import { enableMouse, readMouse, readWheel } from "./mouse.js";
|
|
93
94
|
import { wheelLines } from "./wheelStep.js";
|
|
94
95
|
import { applySelection, ctrlCShouldCopy, isEmpty, selectionText } from "./selection.js";
|
|
96
|
+
import { applyGrowSelection, edgeLines, focusFromPointer, isEmptyGrow, RowStore } from "./selectionGrow.js";
|
|
97
|
+
import { blockSpans, registerBlockBox } from "./blockBoxes.js";
|
|
98
|
+
import { holdFrames, releaseFrames } from "./framebuffer/frameHold.js";
|
|
95
99
|
import { latestScreen, repaintOverlay, setFrameOverlay } from "./framebuffer/overlay.js";
|
|
96
100
|
import { requestFullRepaint } from "./framebuffer/writer.js";
|
|
97
101
|
import { copyToClipboard } from "./clipboard.js";
|
|
98
|
-
import { chatLayout, reflowScroll, growScroll } from "./chatAnchor.js";
|
|
102
|
+
import { chatLayout, reflowScroll, growScroll, revealScroll } from "./chatAnchor.js";
|
|
99
103
|
import { growFill, INLINE_LIVE_RESERVE, NO_FILL } from "./startupFill.js";
|
|
100
104
|
import { setRowsBelowCaret } from "./exitCursor.js";
|
|
101
105
|
import { caretCell } from "./caretPark.js";
|
|
@@ -220,6 +224,21 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
220
224
|
// state: it is painted by the framebuffer overlay rather than by React, so changing it
|
|
221
225
|
// must not cost a render (see the pointer effect).
|
|
222
226
|
const selection = useRef(null);
|
|
227
|
+
// The selection that FOLLOWS THE CONVERSATION, for a drag that begins in the chat (see
|
|
228
|
+
// selectionGrow.ts): it keeps growing while the view scrolls, and copies all of it. A drag
|
|
229
|
+
// that begins anywhere else (the input box, the banner) is the screen selection above.
|
|
230
|
+
const grow = useRef(null);
|
|
231
|
+
// The rows that passed through the screen while it was alive, which is what gets copied.
|
|
232
|
+
const rowStore = useRef(new RowStore());
|
|
233
|
+
// Where the pointer is while the button is down, so scrolling can move the end of the
|
|
234
|
+
// selection even when the pointer itself does not move.
|
|
235
|
+
const pointer = useRef(null);
|
|
236
|
+
const edgeTimer = useRef(null);
|
|
237
|
+
const stopEdge = useCallback(() => {
|
|
238
|
+
if (edgeTimer.current)
|
|
239
|
+
clearInterval(edgeTimer.current);
|
|
240
|
+
edgeTimer.current = null;
|
|
241
|
+
}, []);
|
|
223
242
|
/**
|
|
224
243
|
* Drop the highlight, if there is one, and stop tinting frames.
|
|
225
244
|
*
|
|
@@ -230,12 +249,16 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
230
249
|
* then uninstall: the repaint is what takes the highlight off the screen.
|
|
231
250
|
*/
|
|
232
251
|
const clearSelection = useCallback(() => {
|
|
233
|
-
if (!selection.current)
|
|
252
|
+
if (!selection.current && !grow.current)
|
|
234
253
|
return;
|
|
235
254
|
selection.current = null;
|
|
255
|
+
grow.current = null;
|
|
256
|
+
rowStore.current.clear();
|
|
257
|
+
pointer.current = null;
|
|
258
|
+
stopEdge();
|
|
236
259
|
repaintOverlay();
|
|
237
260
|
setFrameOverlay(null);
|
|
238
|
-
}, []);
|
|
261
|
+
}, [stopEdge]);
|
|
239
262
|
// PromptInput installs the handler that turns a click into a caret position; only it
|
|
240
263
|
// knows what its rows currently hold. Null until the input is on screen.
|
|
241
264
|
const caretClick = useRef(null);
|
|
@@ -336,6 +359,12 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
336
359
|
const [hoverId, setHoverId] = useState(null);
|
|
337
360
|
// Set when a row is opened or folded, read once by the height effect.
|
|
338
361
|
const rowToggled = useRef(false);
|
|
362
|
+
// Which place was pressed, and where the view stood before it was opened and after the view
|
|
363
|
+
// moved to show it, so that folding the same row can put the view back where it was.
|
|
364
|
+
const toggledHit = useRef(null);
|
|
365
|
+
// Whether the row pressed is the NEWEST one that can be opened, the only kind the view follows.
|
|
366
|
+
const toggledNewest = useRef(false);
|
|
367
|
+
const openedFrom = useRef(null);
|
|
339
368
|
const [footerHeight, setFooterHeight] = useState(0);
|
|
340
369
|
// The chat viewport's REAL height. Yoga decides it now (flexGrow beside a
|
|
341
370
|
// flexShrink:0 footer); this is read back purely so the scroll maths knows how
|
|
@@ -1062,6 +1091,12 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1062
1091
|
useInput((input, key) => {
|
|
1063
1092
|
if (!key.ctrl || input !== "c")
|
|
1064
1093
|
return;
|
|
1094
|
+
const g = grow.current;
|
|
1095
|
+
if (g && !isEmptyGrow(g)) {
|
|
1096
|
+
copyGrown(g);
|
|
1097
|
+
clearSelection();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1065
1100
|
const sel = selection.current;
|
|
1066
1101
|
if (ctrlCShouldCopy(sel)) {
|
|
1067
1102
|
const screen = latestScreen();
|
|
@@ -1219,6 +1254,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1219
1254
|
requestFullRepaint();
|
|
1220
1255
|
setScrollUp(maxScrollRef.current);
|
|
1221
1256
|
}
|
|
1257
|
+
// Move what the turn is waiting on to the background, without waiting for the agent
|
|
1258
|
+
// to decide it. Silent when nothing is running (a stray press needs no comment); it
|
|
1259
|
+
// speaks only when something WAS running and could not be moved.
|
|
1260
|
+
else if (key.ctrl && _input === "x") {
|
|
1261
|
+
if (pressBackground(session.current?.toolContext.foregroundCommands) === "stuck") {
|
|
1262
|
+
note("That command could not be moved to the background.");
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1222
1265
|
}, { isActive: ready && overlay === null });
|
|
1223
1266
|
// Leaving the reading view when it reaches the bottom needs no effect of its own:
|
|
1224
1267
|
// `reading` is `scrollUp > 0`, so landing on zero — the wheel, the keys, ctrl+End, or
|
|
@@ -1322,17 +1365,45 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1322
1365
|
// reader's absolute position rather than leave `scrollUp` where it was.
|
|
1323
1366
|
const prev = prevContentHeight.current;
|
|
1324
1367
|
let next = growScroll(scrollUp, prev, height);
|
|
1325
|
-
// A row was just opened or folded.
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1368
|
+
// A row was just opened or folded.
|
|
1369
|
+
//
|
|
1370
|
+
// OPENED, while pinned to the newest: the view moves just far enough to show what opened
|
|
1371
|
+
// (see revealScroll). It used to hold the pressed row in place and let the contents appear
|
|
1372
|
+
// below it, which for the newest row is below the screen: the reader had to scroll to see
|
|
1373
|
+
// what they had asked for. A row opened with the view scrolled back is NOT touched.
|
|
1374
|
+
// FOLDED: the view goes back to where it was before this row was opened, if nothing has
|
|
1375
|
+
// moved it since; otherwise it slides up by what was taken away, so the row stays put.
|
|
1329
1376
|
if (rowToggled.current && height !== prev) {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1377
|
+
const hit = toggledHit.current;
|
|
1378
|
+
if (height > prev) {
|
|
1379
|
+
// Only for the NEWEST row that can be opened, with the reader pinned to the bottom: that
|
|
1380
|
+
// is the one whose contents would be pushed below the screen. Every other row, even one
|
|
1381
|
+
// that is on screen at the time, keeps the old rule (held in place, contents below it).
|
|
1382
|
+
const node = hit !== null && scrollUp === 0 && toggledNewest.current ? revealNode(hit) : undefined;
|
|
1383
|
+
if (hit !== null && node && contentRef.current) {
|
|
1384
|
+
const n = measureElement(node);
|
|
1385
|
+
const c = measureElement(contentRef.current);
|
|
1386
|
+
next = revealScroll(scrollUp, height, chatRows, n.y - c.y, n.height);
|
|
1387
|
+
openedFrom.current = { hit, before: scrollUp, after: next };
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
openedFrom.current = null;
|
|
1391
|
+
if (scrollUp === 0)
|
|
1392
|
+
next = height - prev;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
const from = openedFrom.current;
|
|
1397
|
+
if (from && from.hit === hit && from.after === scrollUp)
|
|
1398
|
+
next = Math.min(from.before, Math.max(0, height - chatRows));
|
|
1399
|
+
else
|
|
1400
|
+
next = Math.max(0, scrollUp - (prev - height));
|
|
1401
|
+
openedFrom.current = null;
|
|
1402
|
+
}
|
|
1334
1403
|
}
|
|
1335
1404
|
rowToggled.current = false;
|
|
1405
|
+
// The view is now placed for the new height; the next frame is the right one.
|
|
1406
|
+
releaseFrames();
|
|
1336
1407
|
if (next !== scrollUp)
|
|
1337
1408
|
setScrollUp(next);
|
|
1338
1409
|
}
|
|
@@ -1448,14 +1519,87 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1448
1519
|
* chance to reflow the screen under a user who is only trying to highlight a word.
|
|
1449
1520
|
* `repaintOverlay` re-tints the frame already on screen instead.
|
|
1450
1521
|
*/
|
|
1522
|
+
// ── the selection that follows the conversation ──────────────────────────────────────────
|
|
1523
|
+
/** The screen rows that show the conversation. */
|
|
1524
|
+
const chatViewRows = useCallback(() => {
|
|
1525
|
+
if (!chatRef.current)
|
|
1526
|
+
return null;
|
|
1527
|
+
const m = measureElement(chatRef.current);
|
|
1528
|
+
return { top: frameTopRef.current + m.y, bottom: frameTopRef.current + m.y + m.height };
|
|
1529
|
+
}, []);
|
|
1530
|
+
/** Runs on every frame while a selection of this kind exists: keep the rows, then paint it. */
|
|
1531
|
+
const growOverlay = useCallback((screen) => {
|
|
1532
|
+
const g = grow.current;
|
|
1533
|
+
const view = chatViewRows();
|
|
1534
|
+
if (!g || !view)
|
|
1535
|
+
return;
|
|
1536
|
+
const spans = blockSpans(frameTopRef.current);
|
|
1537
|
+
if (g.dragging) {
|
|
1538
|
+
// The pointer has not moved but the view has, so what is under it is a different
|
|
1539
|
+
// row: the end of the selection is wherever the pointer is NOW.
|
|
1540
|
+
const p = pointer.current;
|
|
1541
|
+
if (p) {
|
|
1542
|
+
const focus = focusFromPointer(spans, view, p.x, p.y, screen.width);
|
|
1543
|
+
if (focus)
|
|
1544
|
+
g.focus = focus;
|
|
1545
|
+
}
|
|
1546
|
+
const chip = pillHit.current;
|
|
1547
|
+
rowStore.current.capture(screen, spans, view, chip ? { y: chip.row, x0: chip.left, x1: chip.right + 1 } : null);
|
|
1548
|
+
}
|
|
1549
|
+
applyGrowSelection(screen, g, spans, view);
|
|
1550
|
+
}, [chatViewRows]);
|
|
1551
|
+
/** Begin a selection that follows the conversation, if the press landed on it. */
|
|
1552
|
+
const startGrow = useCallback((x, y) => {
|
|
1553
|
+
const view = chatViewRows();
|
|
1554
|
+
const screen = latestScreen();
|
|
1555
|
+
if (!view || !screen || y < view.top || y >= view.bottom)
|
|
1556
|
+
return false;
|
|
1557
|
+
const focus = focusFromPointer(blockSpans(frameTopRef.current), view, x, y, screen.width);
|
|
1558
|
+
if (!focus)
|
|
1559
|
+
return false;
|
|
1560
|
+
grow.current = { anchor: focus, focus, dragging: true };
|
|
1561
|
+
rowStore.current.clear();
|
|
1562
|
+
pointer.current = { x, y };
|
|
1563
|
+
setFrameOverlay(growOverlay);
|
|
1564
|
+
repaintOverlay();
|
|
1565
|
+
return true;
|
|
1566
|
+
}, [chatViewRows, growOverlay]);
|
|
1567
|
+
/** While the pointer is held past an edge, keep scrolling, so the selection keeps growing. */
|
|
1568
|
+
const keepScrolling = useCallback(() => {
|
|
1569
|
+
if (edgeTimer.current)
|
|
1570
|
+
return;
|
|
1571
|
+
edgeTimer.current = setInterval(() => {
|
|
1572
|
+
const view = chatViewRows();
|
|
1573
|
+
const p = pointer.current;
|
|
1574
|
+
if (!grow.current?.dragging || !view || !p)
|
|
1575
|
+
return stopEdge();
|
|
1576
|
+
const lines = edgeLines(p.y, view, latestScreen()?.height ?? process.stdout.rows ?? 24);
|
|
1577
|
+
if (lines === 0)
|
|
1578
|
+
return stopEdge();
|
|
1579
|
+
scrollBy(lines);
|
|
1580
|
+
}, EDGE_SCROLL_MS);
|
|
1581
|
+
}, [chatViewRows, scrollBy, stopEdge]);
|
|
1582
|
+
/** Copy everything between the two ends, and say so if some of it could not be. */
|
|
1583
|
+
function copyGrown(g) {
|
|
1584
|
+
const s = stateRef.current;
|
|
1585
|
+
const ids = [...s.committed, ...s.tail].map((b) => b.id);
|
|
1586
|
+
const grown = rowStore.current.text(g, ids, (id) => blockHeights.current.get(id)?.height);
|
|
1587
|
+
if (grown.text)
|
|
1588
|
+
copyToClipboard(grown.text);
|
|
1589
|
+
if (!grown.complete)
|
|
1590
|
+
note("Part of what you selected scrolled past too fast to be copied. Select it again, scrolling more slowly.");
|
|
1591
|
+
}
|
|
1451
1592
|
// Mouse reporting itself is switched on and off by — it belongs to
|
|
1452
1593
|
// the shell, not to this component, because the inline shell must never have it on.
|
|
1453
1594
|
// What is left here is the highlight, which has to come down when the app unmounts.
|
|
1454
1595
|
useEffect(() => {
|
|
1455
1596
|
if (!ready)
|
|
1456
1597
|
return;
|
|
1457
|
-
return () =>
|
|
1458
|
-
|
|
1598
|
+
return () => {
|
|
1599
|
+
stopEdge();
|
|
1600
|
+
setFrameOverlay(null);
|
|
1601
|
+
};
|
|
1602
|
+
}, [ready, stopEdge]);
|
|
1459
1603
|
/**
|
|
1460
1604
|
* Everything the pointer does, read through `useInput`.
|
|
1461
1605
|
*
|
|
@@ -1485,14 +1629,23 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1485
1629
|
// This is the same rule the input box already follows for keystrokes — one event
|
|
1486
1630
|
// in, one render out — applied to the other thing that arrives in bursts.
|
|
1487
1631
|
if (notches.length > 0) {
|
|
1488
|
-
// Content moves out from under a selection when the view scrolls, so the
|
|
1489
|
-
// highlight would be sitting on text that is no longer the text it copied.
|
|
1490
|
-
|
|
1632
|
+
// Content moves out from under a SCREEN selection when the view scrolls, so the
|
|
1633
|
+
// highlight would be sitting on text that is no longer the text it copied. One that
|
|
1634
|
+
// follows the conversation stays, and grows if the button is still down.
|
|
1635
|
+
if (!grow.current)
|
|
1636
|
+
clearSelection();
|
|
1491
1637
|
// Signed notch count, then the distance for it. The same distance for every notch,
|
|
1492
1638
|
// applied at once (wheelStep.ts): how far the view goes follows how far the wheel
|
|
1493
1639
|
// was turned, and nothing else.
|
|
1494
1640
|
const net = notches.reduce((n, dir) => n + (dir === "up" ? 1 : -1), 0);
|
|
1495
|
-
|
|
1641
|
+
let lines = wheelLines(net);
|
|
1642
|
+
// With a selection alive no row may be scrolled past unseen: a step is never more than
|
|
1643
|
+
// the view is tall, or the rows in between could not be copied.
|
|
1644
|
+
if (grow.current) {
|
|
1645
|
+
const view = chatViewRows();
|
|
1646
|
+
const room = view ? Math.max(1, view.bottom - view.top - 1) : 16;
|
|
1647
|
+
lines = Math.max(-room, Math.min(room, lines));
|
|
1648
|
+
}
|
|
1496
1649
|
// The wheel is how anyone actually scrolls, so in the inline shell it is what
|
|
1497
1650
|
// opens the reading view. Turning it UP is the gesture: the reader is going back
|
|
1498
1651
|
// through the conversation and wants the prompt to stay where they can type into
|
|
@@ -1536,12 +1689,28 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1536
1689
|
setScrollUp(0);
|
|
1537
1690
|
continue;
|
|
1538
1691
|
}
|
|
1692
|
+
// A press on the conversation starts a selection that follows it. Anywhere else (the
|
|
1693
|
+
// input box, the banner) it is the screen selection, which is what edits the input.
|
|
1694
|
+
if (startGrow(event.x, event.y)) {
|
|
1695
|
+
selection.current = null;
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
grow.current = null;
|
|
1699
|
+
rowStore.current.clear();
|
|
1700
|
+
pointer.current = null;
|
|
1701
|
+
stopEdge();
|
|
1539
1702
|
selection.current = { anchor: { x: event.x, y: event.y }, focus: { x: event.x, y: event.y } };
|
|
1540
1703
|
// Installed here rather than for the life of the session: see clearSelection.
|
|
1541
1704
|
setFrameOverlay((screen) => applySelection(screen, selection.current));
|
|
1542
1705
|
repaintOverlay();
|
|
1543
1706
|
}
|
|
1544
1707
|
else if (event.kind === "drag") {
|
|
1708
|
+
if (grow.current?.dragging) {
|
|
1709
|
+
pointer.current = { x: event.x, y: event.y };
|
|
1710
|
+
pendingDrag = true;
|
|
1711
|
+
keepScrolling();
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1545
1714
|
if (!selection.current)
|
|
1546
1715
|
continue;
|
|
1547
1716
|
selection.current = { anchor: selection.current.anchor, focus: { x: event.x, y: event.y } };
|
|
@@ -1549,6 +1718,34 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1549
1718
|
}
|
|
1550
1719
|
else {
|
|
1551
1720
|
pendingDrag = false;
|
|
1721
|
+
const g = grow.current;
|
|
1722
|
+
if (g) {
|
|
1723
|
+
stopEdge();
|
|
1724
|
+
pointer.current = null;
|
|
1725
|
+
g.dragging = false;
|
|
1726
|
+
const view = chatViewRows();
|
|
1727
|
+
const screen = latestScreen();
|
|
1728
|
+
if (view && screen) {
|
|
1729
|
+
const focus = focusFromPointer(blockSpans(frameTopRef.current), view, event.x, event.y, screen.width);
|
|
1730
|
+
if (focus)
|
|
1731
|
+
g.focus = focus;
|
|
1732
|
+
}
|
|
1733
|
+
if (isEmptyGrow(g)) {
|
|
1734
|
+
// A click, not a drag: take the highlight down, then do what a click does.
|
|
1735
|
+
clearSelection();
|
|
1736
|
+
const hitId = expandableAt(event.y, frameTopRef.current, chatRef.current);
|
|
1737
|
+
if (hitId !== null) {
|
|
1738
|
+
toggleRow(hitId);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
placeCaretAt(event.x, event.y);
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
// Copied on release, so selecting IS copying; the highlight stays as the receipt.
|
|
1745
|
+
copyGrown(g);
|
|
1746
|
+
repaintOverlay();
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1552
1749
|
const sel = selection.current;
|
|
1553
1750
|
if (!sel)
|
|
1554
1751
|
continue;
|
|
@@ -1602,6 +1799,11 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1602
1799
|
/** Open or fold a row, keeping it where the reader pressed it. See the height effect. */
|
|
1603
1800
|
function toggleRow(hit) {
|
|
1604
1801
|
rowToggled.current = true;
|
|
1802
|
+
toggledHit.current = hit;
|
|
1803
|
+
toggledNewest.current = lastExpandable(stateRef.current) === hitBlock(hit);
|
|
1804
|
+
// Opening or folding takes two renders, and the first is drawn with the view still placed
|
|
1805
|
+
// for the old height. Keep that one off the screen; the effect below releases it.
|
|
1806
|
+
holdFrames();
|
|
1605
1807
|
// A hit on one command inside a commands row opens that command; anything else is a row.
|
|
1606
1808
|
if (hitItem(hit) >= 0)
|
|
1607
1809
|
dispatch({ type: "toggleItem", id: hitBlock(hit), index: hitItem(hit) });
|
|
@@ -3439,7 +3641,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3439
3641
|
const completions = [
|
|
3440
3642
|
...BASE_COMMANDS,
|
|
3441
3643
|
...skills.map((s) => ({ name: `/${s.name}`, description: s.description || "project skill" })),
|
|
3442
|
-
...mcpPrompts.map((p) => ({ name: promptCommand(p), description: p.description || `prompt from ${p.server}
|
|
3644
|
+
...mcpPrompts.map((p) => ({ name: promptCommand(p), description: p.description || `prompt from ${p.server}`, quiet: true })),
|
|
3443
3645
|
];
|
|
3444
3646
|
// While an overlay is open it renders in the menu slot below the input and owns the
|
|
3445
3647
|
// keyboard; the input box itself stays visible but inert. `maxRows` is the App's
|
|
@@ -3676,7 +3878,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3676
3878
|
// to `chatAnchor.ts` so the rule is unit-tested rather than eyeballed — see there
|
|
3677
3879
|
// for why a short transcript now rests ON the input box instead of stranding
|
|
3678
3880
|
// itself at the top of the screen, and why that cannot disturb a scrolled frame.
|
|
3679
|
-
const { marginTop: chatOffset,
|
|
3881
|
+
const { marginTop: chatOffset, maxScroll, scrolled } = chatLayout(contentHeight, chatRows, scrollUp);
|
|
3680
3882
|
// Published for the scroll handlers, which run between frames and cannot compute it.
|
|
3681
3883
|
maxScrollRef.current = maxScroll;
|
|
3682
3884
|
// The chip that says the view is not at the bottom. `scrolled` and not `scrollUp`:
|
|
@@ -3753,6 +3955,15 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3753
3955
|
// carried off by a resize they did not intend as navigation. Applied once the new
|
|
3754
3956
|
// height has actually been measured — see the effect that consumes this.
|
|
3755
3957
|
reflowFrom.current = { scrolled: scrollUp, maxScroll: Math.max(0, contentHeight - chatRows) };
|
|
3958
|
+
// A re-wrap changes how many rows every block has, so the rows kept for a selection no
|
|
3959
|
+
// longer line up with what is on screen. Dropped without a repaint (this is a render).
|
|
3960
|
+
if (grow.current) {
|
|
3961
|
+
grow.current = null;
|
|
3962
|
+
rowStore.current.clear();
|
|
3963
|
+
pointer.current = null;
|
|
3964
|
+
stopEdge();
|
|
3965
|
+
setFrameOverlay(null);
|
|
3966
|
+
}
|
|
3756
3967
|
}
|
|
3757
3968
|
// Only a MEASURED prefix can be virtualized: a block whose height is unknown cannot
|
|
3758
3969
|
// be replaced by a spacer, because there is no honest number to give the spacer.
|
|
@@ -3815,18 +4026,31 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3815
4026
|
// Yoga sizes against a pinned footer, with the transcript offset inside it. Kept as
|
|
3816
4027
|
// two copies they would drift apart the first time either was touched, and the drift
|
|
3817
4028
|
// would show as scrolling behaving differently in one shell than in the other.
|
|
3818
|
-
|
|
4029
|
+
// Pinned to the newest, the transcript is ALIGNED to the bottom of the viewport by the layout
|
|
4030
|
+
// itself (flex-end) instead of being offset by a number. The number comes from measurements
|
|
4031
|
+
// that lag a frame: when the footer grows (a menu opens, the input takes a second line) or a
|
|
4032
|
+
// row lands, the first frame was drawn with the old numbers, so for a moment the view showed
|
|
4033
|
+
// the wrong part of the conversation. Yoga decides alignment in the same pass that sizes the
|
|
4034
|
+
// frame, so there is nothing stale to be wrong about. A short conversation rests on the
|
|
4035
|
+
// footer by the same rule, which is what the spacer that used to be here did.
|
|
4036
|
+
//
|
|
4037
|
+
// Scrolled back, the offset has to be a number again (nothing but a distance from the top
|
|
4038
|
+
// can hold a position while content is appended), and `growScroll` keeps it right.
|
|
4039
|
+
const pinnedView = scrolled === 0;
|
|
4040
|
+
const chatView = (_jsxs(Box, { ref: chatRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 1, overflow: "hidden", justifyContent: pinnedView ? "flex-end" : "flex-start", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, marginTop: pinnedView ? 0 : chatOffset, children: _jsxs(Box, { ref: contentRef, flexDirection: "column", flexShrink: 0, children: [win.padTop > 0 ? _jsx(Box, { flexShrink: 0, height: win.padTop }) : null, rendered.slice(win.start, win.end).map((b, i) => {
|
|
3819
4041
|
const idx = win.start + i;
|
|
3820
4042
|
return (
|
|
3821
4043
|
// flexShrink:0 is load-bearing — without it Yoga compresses an
|
|
3822
4044
|
// overfull column and silently drops rows out of the middle.
|
|
3823
4045
|
_jsx(Box, { ref: (node) => {
|
|
4046
|
+
registerBlockBox(b.id, node);
|
|
3824
4047
|
if (node && needsMeasure(blockHeights.current.get(b.id), b))
|
|
3825
4048
|
toMeasure.current.set(b, node);
|
|
3826
4049
|
}, 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));
|
|
3827
4050
|
}), padMiddle > 0 ? _jsx(Box, { flexShrink: 0, height: padMiddle }) : null, rendered.slice(known).map((b, i) => {
|
|
3828
4051
|
const idx = known + i;
|
|
3829
4052
|
return (_jsx(Box, { ref: (node) => {
|
|
4053
|
+
registerBlockBox(b.id, node);
|
|
3830
4054
|
if (node && needsMeasure(blockHeights.current.get(b.id), b))
|
|
3831
4055
|
toMeasure.current.set(b, node);
|
|
3832
4056
|
}, 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));
|
|
@@ -4347,6 +4571,8 @@ function StatusLine({ busy, startedAt, lastMs, usage, received, advance, waiting
|
|
|
4347
4571
|
// component rendering nothing at all. Two would read as a hole.
|
|
4348
4572
|
return (_jsxs(Box, { marginBottom: 0, children: [dot, label] }));
|
|
4349
4573
|
}
|
|
4574
|
+
/** How often the view scrolls while a drag is held past the edge of the conversation. */
|
|
4575
|
+
const EDGE_SCROLL_MS = 45;
|
|
4350
4576
|
/** The working line's render clock, in milliseconds. See StatusLine's tick. */
|
|
4351
4577
|
const LIVE_TICK_MS = 16;
|
|
4352
4578
|
function fmtElapsed(s) {
|