mindweave 3.2.3 → 3.2.5

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
@@ -69,6 +69,7 @@ import { APP_EXPLANATION, APP_PAGE_URL, appAnnouncement, openInBrowser } from ".
69
69
  import { PromptInput } from "./components/PromptInput.js";
70
70
  import { Picker } from "./components/Picker.js";
71
71
  import { ApprovalBox } from "./components/ApprovalBox.js";
72
+ import { approvalAnswer, approvalBlock, workedSeconds } from "./approvalText.js";
72
73
  import { BlockView } from "./components/BlockView.js";
73
74
  import { initialState, lastExpandable, reduce, trimNarration } from "./transcript.js";
74
75
  import { isTight } from "./blockSpacing.js";
@@ -358,6 +359,10 @@ export function App({ resumeSessionId, initialScreen }) {
358
359
  const [lastMs, setLastMs] = useState(null);
359
360
  const [taskUsage, setTaskUsage] = useState(null);
360
361
  const turnStart = useRef(null);
362
+ // Time spent waiting for the person to answer a question, which is not time the agent worked: the working
363
+ // line's clock leaves it out, and says it is waiting while it lasts. `waitSince` is when the current wait began.
364
+ const waitedMs = useRef(0);
365
+ const waitSince = useRef(null);
361
366
  // Token accounting for the status line. A task (one turn) may span several model
362
367
  // calls; we keep each call's REAL usage and fold it into a meaningful summary
363
368
  // (context size + cache-aware cost), not a raw sum of re-sent context — that sum
@@ -558,6 +563,16 @@ export function App({ resumeSessionId, initialScreen }) {
558
563
  // An interactive overlay (session picker, model/think chooser, or an approval
559
564
  // prompt). When set, it owns the keyboard and the input box is hidden.
560
565
  const [overlay, setOverlay] = useState(null);
566
+ // While a question is open the working line stands still (see StatusLine), so the wait is counted here.
567
+ useEffect(() => {
568
+ const open = overlay?.kind === "approval";
569
+ if (open && waitSince.current == null)
570
+ waitSince.current = Date.now();
571
+ if (!open && waitSince.current != null) {
572
+ waitedMs.current += Date.now() - waitSince.current;
573
+ waitSince.current = null;
574
+ }
575
+ }, [overlay]);
561
576
  // Set while a command that opens one of those surfaces is on its way, so the box that
562
577
  // holds them stays on screen for the gap between the command list closing and the
563
578
  // surface appearing. See `OVERLAY_COMMANDS`.
@@ -587,7 +602,11 @@ export function App({ resumeSessionId, initialScreen }) {
587
602
  if (body) {
588
603
  // Titled → a facts block on a rail, rendered verbatim (a command must not be
589
604
  // reinterpreted as markdown). Untitled → prose, because it is a document to read.
590
- dispatch(detailTitle ? { type: "notice", title: detailTitle, body } : { type: "say", text: body });
605
+ //
606
+ // The question goes in the block with what it is about. The block used to hold the command alone, and
607
+ // the question lived only in the box at the bottom, which is gone once it is answered: what was left
608
+ // in the conversation was a command with no word about why it had been asked or how it was answered.
609
+ dispatch(detailTitle ? { type: "notice", title: detailTitle, body: approvalBlock(question, body) } : { type: "say", text: body });
591
610
  }
592
611
  const answer = approvals.current.ask({
593
612
  kind: "approval",
@@ -599,6 +618,15 @@ export function App({ resumeSessionId, initialScreen }) {
599
618
  ...(freeText ? { freeText } : {}),
600
619
  });
601
620
  showNextApproval.current();
621
+ // And what was answered, once it is, so the record reads as a question and its answer. Only for a
622
+ // question that printed a block above; a plain one-line question leaves nothing to answer later.
623
+ if (body && detailTitle) {
624
+ void answer.then((choice) => {
625
+ const line = approvalAnswer(choice);
626
+ if (line)
627
+ note(line);
628
+ });
629
+ }
602
630
  return answer;
603
631
  });
604
632
  // Bumped whenever a background shell starts/finishes, to re-render the indicator.
@@ -998,6 +1026,8 @@ export function App({ resumeSessionId, initialScreen }) {
998
1026
  // Start/stop a turn's timer (drives the persistent status line).
999
1027
  function startTurn() {
1000
1028
  turnStart.current = Date.now();
1029
+ waitedMs.current = 0;
1030
+ waitSince.current = null;
1001
1031
  usageSamples.current = [];
1002
1032
  meter.current = meterReset();
1003
1033
  setTaskUsage(null); // clear the previous task's summary while this one runs
@@ -3756,7 +3786,7 @@ export function App({ resumeSessionId, initialScreen }) {
3756
3786
  // The status line, the queued bar, the input and whatever sits under it. Built once
3757
3787
  // and rendered by BOTH shells: the only thing that differs between them is where it
3758
3788
  // ends up — pinned to the bottom of a frame we own, or simply the last thing printed.
3759
- 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, startedAt: turnStart.current, lastMs: lastMs, usage: taskUsage, received: liveTokens, advance: advanceTokens }) }), _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) => {
3789
+ 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, 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) => {
3760
3790
  caretClick.current = place;
3761
3791
  }, registerTextSelect: (select) => {
3762
3792
  textSelect.current = select;
@@ -4245,7 +4275,7 @@ function timeAgo(ms) {
4245
4275
  * line above separating it from the conversation, where the
4246
4276
  * receipt belongs to the prompt area, not glued to the last reply.
4247
4277
  */
4248
- function StatusLine({ busy, startedAt, lastMs, usage, received, advance, }) {
4278
+ function StatusLine({ busy, startedAt, lastMs, usage, received, advance, waiting, waited, }) {
4249
4279
  // The render clock while busy: it advances the elapsed timer AND steps the eased token
4250
4280
  // counter one frame. It runs at about the display's pace (16ms): the number reads as counting
4251
4281
  // rather than as jumping, and the working dot moves smoothly. It was 50ms (20 frames a
@@ -4271,14 +4301,14 @@ function StatusLine({ busy, startedAt, lastMs, usage, received, advance, }) {
4271
4301
  // are what make it read as a live gauge rather than a sentence, and the verb is
4272
4302
  // swapped every few seconds (see workingVerb) and steady inside each, so the line does not flicker between
4273
4303
  // words while the seconds tick.
4274
- const secs = Math.floor((Date.now() - startedAt) / 1000);
4304
+ const secs = workedSeconds(Date.now(), startedAt, waited());
4275
4305
  // OUTPUT tokens for THIS task, estimated from what has streamed back. It is the task's
4276
4306
  // own work: the re-sent conversation (the whole session's context, re-billed each tool
4277
4307
  // round) is NOT counted here, because that is session overhead, not what this task
4278
4308
  // did. Output is
4279
4309
  // also the only thing that grows continuously while a turn runs, so it animates cleanly.
4280
4310
  const got = received();
4281
- label = (_jsxs(Text, { children: [" ", workingVerb(startedAt, Date.now() - startedAt), "\u2026", " ", _jsxs(Text, { dimColor: true, children: ["< ", fmtElapsed(secs), got > 0 ? ` · ↓ ${formatTokens(got)} tokens` : "", " >"] })] }));
4311
+ label = (_jsxs(Text, { children: [" ", waiting ? "Waiting for your answer" : workingVerb(startedAt, Date.now() - startedAt - waited()), "\u2026", " ", _jsxs(Text, { dimColor: true, children: ["< ", fmtElapsed(secs), got > 0 ? ` · ↓ ${formatTokens(got)} tokens` : "", " >"] })] }));
4282
4312
  }
4283
4313
  else if (lastMs != null) {
4284
4314
  // Settled receipt: elapsed time and the turn's real token cost, shown at once,