mindweave 2.4.2 → 2.4.4

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 CHANGED
@@ -74,6 +74,7 @@ import { currentInstall, requestRestart, runUpdate } from "./updateRunner.js";
74
74
  import { enableMouse, readMouse, readWheel } from "./mouse.js";
75
75
  import { applySelection, ctrlCShouldCopy, isEmpty, selectionText } from "./selection.js";
76
76
  import { latestScreen, repaintOverlay, setFrameOverlay } from "./framebuffer/overlay.js";
77
+ import { requestFullRepaint } from "./framebuffer/writer.js";
77
78
  import { copyToClipboard } from "./clipboard.js";
78
79
  import { chatLayout, reflowScroll } from "./chatAnchor.js";
79
80
  import { growFill, INLINE_LIVE_RESERVE, NO_FILL } from "./startupFill.js";
@@ -651,6 +652,14 @@ export function App({ resumeSessionId, initialScreen }) {
651
652
  // nothing else on screen would tell them their dev server had fallen over.
652
653
  if (kind === "ready")
653
654
  continue;
655
+ // The watchdog flagged a running shell as stuck — a prompt it is blocked on, or a
656
+ // long silence on a command that should be working. Worth a line: otherwise it sits
657
+ // invisible until it times out.
658
+ if (kind === "stalled") {
659
+ const why = sh.stallReason === "prompt" ? "waiting for input?" : "no output for a while — stuck?";
660
+ addTool(`shell #${sh.id} (${clipCmd(sh.command)}) ${why}`, { error: true });
661
+ continue;
662
+ }
654
663
  if (sh.status === "killed" && sh.stoppedBy === "user")
655
664
  continue;
656
665
  const verb = sh.status === "killed" ? "killed" : `finished — exit ${sh.exitCode}`;
@@ -666,7 +675,7 @@ export function App({ resumeSessionId, initialScreen }) {
666
675
  note(notice);
667
676
  }
668
677
  function attachApproval(s) {
669
- s.toolContext.requestApproval = (q, o, detail, title) => askApproval.current(q, o, detail, title);
678
+ s.toolContext.requestApproval = (q, o, detail, title, freeText) => askApproval.current(q, o, detail, title, freeText);
670
679
  // Same stop Esc performs. A tool reaches for this when the USER has said, by
671
680
  // dismissing a question, that the work should not carry on without them.
672
681
  s.toolContext.interrupt = () => abortRef.current?.abort();
@@ -978,6 +987,11 @@ export function App({ resumeSessionId, initialScreen }) {
978
987
  // scrolled back down before the view moved at all. A flick or two past the top bought
979
988
  // a second of a wheel that did nothing, which reads as the app having frozen.
980
989
  const scrollBy = useCallback((lines) => {
990
+ // Repaint the whole next frame. A scroll can move a row out from under the
991
+ // framebuffer's model (see requestFullRepaint), which is what left a transcript row
992
+ // stranded on the pinned banner; a scroll already redraws almost every visible row, so
993
+ // redrawing the stable ones on top is nearly free and stops that.
994
+ requestFullRepaint();
981
995
  setScrollUp((s) => Math.max(0, Math.min(maxScrollRef.current, s + lines)));
982
996
  }, []);
983
997
  /**
@@ -1030,10 +1044,14 @@ export function App({ resumeSessionId, initialScreen }) {
1030
1044
  // back (see scrollPill.ts) names ctrl+End, so this is the half that makes the
1031
1045
  // chip true. CTRL is what keeps them off the input: plain End and Home belong to
1032
1046
  // the caret, whether or not the input claims them yet.
1033
- else if (key.end && key.ctrl)
1047
+ else if (key.end && key.ctrl) {
1048
+ requestFullRepaint();
1034
1049
  setScrollUp(0);
1035
- else if (key.home && key.ctrl)
1050
+ }
1051
+ else if (key.home && key.ctrl) {
1052
+ requestFullRepaint();
1036
1053
  setScrollUp(maxScrollRef.current);
1054
+ }
1037
1055
  }, { isActive: ready && overlay === null });
1038
1056
  // Leaving the reading view when it reaches the bottom needs no effect of its own:
1039
1057
  // `reading` is `scrollUp > 0`, so landing on zero — the wheel, the keys, ctrl+End, or
@@ -1307,6 +1325,7 @@ export function App({ resumeSessionId, initialScreen }) {
1307
1325
  const hit = pillHit.current;
1308
1326
  if (hit && hitsPill(hit, event.x, event.y)) {
1309
1327
  clearSelection();
1328
+ requestFullRepaint();
1310
1329
  setScrollUp(0);
1311
1330
  continue;
1312
1331
  }
@@ -1522,6 +1541,11 @@ export function App({ resumeSessionId, initialScreen }) {
1522
1541
  const wait = revealWait({ flush: flush.current });
1523
1542
  pumpTimer.current = setTimeout(() => {
1524
1543
  pumpTimer.current = null;
1544
+ // A revealed block shifts the transcript, which can move a row out from under the
1545
+ // framebuffer's model and strand it on the pinned banner (see requestFullRepaint).
1546
+ // Set BEFORE the dispatch, so the very frame that shows the new block redraws the
1547
+ // banner too rather than leaving a stray row on it until the next paint.
1548
+ requestFullRepaint();
1525
1549
  reveal();
1526
1550
  lastRevealAt.current = Date.now();
1527
1551
  pump();
@@ -1534,8 +1558,12 @@ export function App({ resumeSessionId, initialScreen }) {
1534
1558
  const a = revealQ.current.shift();
1535
1559
  if (a.type === "token")
1536
1560
  applySilent(a);
1537
- else
1561
+ else {
1562
+ // An in-place resolve grows a row into its result, shifting everything below it —
1563
+ // the same de-sync risk a reveal carries, so heal the banner on the same frame.
1564
+ requestFullRepaint();
1538
1565
  dispatch(a);
1566
+ }
1539
1567
  // A group stays open once shown; anything that isn't part of it (a
1540
1568
  // standalone tool, narration, a sub-agent) closes it, mirroring exactly
1541
1569
  // what the transcript reducer's own closeToolGroup does on the same actions.
@@ -3764,11 +3792,33 @@ export function mcpDetail(server, blocked = 0) {
3764
3792
  * The port comes from the process's own startup line (see detectPort), so it is
3765
3793
  * shown only when the server actually said where it is listening. */
3766
3794
  function BackgroundBar({ shells }) {
3795
+ // Tick once a second while anything runs, so the elapsed clock beside each command
3796
+ // actually moves — that motion is the whole point: it says the command is still
3797
+ // working, not wedged. The interval is torn down the moment nothing is running.
3798
+ const [, setTick] = useState(0);
3799
+ useEffect(() => {
3800
+ if (shells.length === 0)
3801
+ return;
3802
+ const id = setInterval(() => setTick((t) => t + 1), 1000);
3803
+ return () => clearInterval(id);
3804
+ }, [shells.length]);
3767
3805
  if (shells.length === 0)
3768
3806
  return null;
3769
- const cmds = shells.map((s) => `$ ${clipCmd(s.command)}${s.port ? ` (${s.port})` : ""}`).join(" • ");
3807
+ const now = Date.now();
3808
+ const cmds = shells
3809
+ .map((s) => `$ ${clipCmd(s.command)}${s.port ? ` (${s.port})` : ""} ${bgClock(now - s.startedAt)}`)
3810
+ .join(" • ");
3770
3811
  return (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[BG] " }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: `${shells.length} running: ${cmds}` })] }));
3771
3812
  }
3813
+ /** Elapsed for the background bar: whole seconds under a minute, `1m 20s` past it. */
3814
+ function bgClock(ms) {
3815
+ const seconds = Math.max(0, Math.round(ms / 1000));
3816
+ if (seconds < 60)
3817
+ return `${seconds}s`;
3818
+ const m = Math.floor(seconds / 60);
3819
+ const s = seconds % 60;
3820
+ return s === 0 ? `${m}m` : `${m}m ${s}s`;
3821
+ }
3772
3822
  // One short, useful line rendered inside the input box (see PromptInput's
3773
3823
  // `tip` prop) — picked once per session, not rewritten every render. The
3774
3824
  // mode/model/thinking readout already lives in the header, so this slot is