march-cli 0.1.45 → 0.1.46

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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/cache.mjs +13 -5
  3. package/src/agent/code-search/engine.mjs +7 -2
  4. package/src/agent/code-search/retrieval/safetensors.mjs +16 -10
  5. package/src/agent/code-search/scanner.mjs +11 -5
  6. package/src/agent/model-payload-dumper.mjs +1 -1
  7. package/src/agent/runner/payload/provider-payload-transform.mjs +59 -0
  8. package/src/agent/runner/recall/mid-turn-recall-bridge.mjs +23 -0
  9. package/src/agent/runner.mjs +28 -27
  10. package/src/agent/runtime/remote-ui-client.mjs +1 -1
  11. package/src/agent/runtime/resource/context-resource-loader.mjs +17 -0
  12. package/src/agent/runtime/runtime-factory.mjs +5 -1
  13. package/src/agent/runtime/state/runner-state.mjs +10 -3
  14. package/src/agent/runtime/ui-event-bridge.mjs +2 -2
  15. package/src/agent/turn/turn-runner.mjs +20 -16
  16. package/src/cli/fallback-ui.mjs +2 -2
  17. package/src/cli/repl-loop.mjs +5 -4
  18. package/src/cli/startup/app-runtime.mjs +2 -3
  19. package/src/cli/tui/input/mouse-selection-controller.mjs +19 -0
  20. package/src/cli/tui/output/selectable-copy.mjs +3 -3
  21. package/src/cli/tui/output-buffer.mjs +18 -0
  22. package/src/cli/tui/recall-rendering.mjs +30 -8
  23. package/src/cli/tui/selection/ansi-range.mjs +88 -0
  24. package/src/cli/tui/selection-screen.mjs +31 -99
  25. package/src/cli/turn/turn-input-preparer.mjs +9 -2
  26. package/src/cli/ui.mjs +2 -2
  27. package/src/context/engine.mjs +13 -3
  28. package/src/memory/markdown/semantic-preload.mjs +9 -0
  29. package/src/memory/markdown/semantic-recall.mjs +10 -6
  30. package/src/memory/markdown-store.mjs +19 -11
  31. package/src/web-ui/dist/assets/{index-CcbYCcWs.css → index-BG1Pxf1k.css} +1 -1
  32. package/src/web-ui/dist/assets/{index-CBYbNVgs.js → index-C0xOHlDz.js} +1 -1
  33. package/src/web-ui/dist/index.html +2 -2
  34. package/src/web-ui/runtime-host.mjs +15 -3
  35. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +9 -6
  36. package/src/web-ui/src/model.ts +2 -2
  37. package/src/web-ui/src/runtime/client.ts +1 -1
  38. package/src/web-ui/src/runtime/runtimeTimeline.ts +2 -2
  39. package/src/web-ui/src/styles/shell.css +1 -0
  40. package/src/web-ui/src/timelineAdapter.ts +1 -1
@@ -19,7 +19,7 @@ export async function runSingleShotPrompt({
19
19
  const turnInput = await prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
20
20
  ui.writeln(turnInput.displayMessage);
21
21
  ui.recall?.({ hints: turnInput.userRecallHints, report: turnInput.userRecallReport });
22
- if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ hints: turnInput.carryoverRecallHints });
22
+ if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ hints: turnInput.carryoverRecallHints, report: turnInput.carryoverRecallReport, variant: "assistant" });
23
23
  refreshStatusBar.startWorking?.();
24
24
  const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
25
25
  renderPendingAssistantRecallPreview({ runner, ui });
@@ -188,7 +188,7 @@ async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, re
188
188
  const turnInput = await prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
189
189
  ui.writeln(turnInput.displayMessage);
190
190
  ui.recall?.({ hints: turnInput.userRecallHints, report: turnInput.userRecallReport });
191
- if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ hints: turnInput.carryoverRecallHints });
191
+ if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ hints: turnInput.carryoverRecallHints, report: turnInput.carryoverRecallReport, variant: "assistant" });
192
192
  setTurnRunning(true);
193
193
  refreshStatusBar.startWorking?.();
194
194
  const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
@@ -215,7 +215,8 @@ async function handleTurnLifecycleAction(action, { runner, ui }) {
215
215
  function renderPendingAssistantRecallPreview({ runner, ui }) {
216
216
  if (runner.engine.hasRenderedPendingAssistantRecallHints?.()) return;
217
217
  const hints = runner.engine.peekPendingAssistantRecallHints?.() ?? [];
218
- if (hints.length === 0) return;
219
- ui.recall?.({ hints });
218
+ const report = runner.engine.peekPendingAssistantRecallReport?.() ?? null;
219
+ if (hints.length === 0 && !report) return;
220
+ ui.recall?.({ hints, report, variant: "assistant" });
220
221
  runner.engine.markPendingAssistantRecallHintsRendered?.();
221
222
  }
@@ -11,7 +11,7 @@ import { createMarchAuthStorage } from "../../auth/storage.mjs";
11
11
  import { createRuntimeRunner } from "./create-runtime-runner.mjs";
12
12
  import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
13
13
  import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
14
- import { preloadSemanticMemoryRecall } from "../../memory/markdown/semantic-preload.mjs";
14
+ import { startSemanticMemoryRecallPreload } from "../../memory/markdown/semantic-preload.mjs";
15
15
  import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
16
16
  import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
17
17
  import { loadOrCreateProjectId, resumeStartupSession } from "./startup-session.mjs";
@@ -91,8 +91,6 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
91
91
  shellRuntime,
92
92
  historyStore: inputHistoryStore,
93
93
  });
94
- await preloadSemanticMemoryRecall({ memoryStore, ui, logger });
95
-
96
94
  const outputRouter = createWorkspaceOutputRouter({
97
95
  ui,
98
96
  activeProjectId: currentProjectInfo.projectId,
@@ -143,6 +141,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
143
141
  return { ok: false, code: 1, logger };
144
142
  }
145
143
  syncRuntimeSessionStateFromRunner(sessionState, runner, sessionsRoot);
144
+ startSemanticMemoryRecallPreload({ memoryStore, logger, delayMs: 1000 });
146
145
 
147
146
  const initialRuntime = {
148
147
  project: currentProjectInfo,
@@ -12,6 +12,7 @@ export function createMouseSelectionController({
12
12
  writeClipboard,
13
13
  requestRender,
14
14
  }) {
15
+ let leftDown = null;
15
16
  function copySelectionText(text) {
16
17
  if (!text) return false;
17
18
  let result;
@@ -43,6 +44,12 @@ export function createMouseSelectionController({
43
44
  requestRender();
44
45
  }
45
46
 
47
+ function toggleOutputToolCard(mouse) {
48
+ const hit = selection.hitTest?.(mouse);
49
+ if (hit?.regionId !== "output") return false;
50
+ return output.toggleToolCardAtVisibleRow?.(hit.row, terminal.columns || 80) === true;
51
+ }
52
+
46
53
  return {
47
54
  handleMouseInput(data) {
48
55
  const mouse = parseMouseEvent(data);
@@ -56,6 +63,7 @@ export function createMouseSelectionController({
56
63
  return { consume: true };
57
64
  }
58
65
  if (mouse?.type === "down" && mouse.button === 0) {
66
+ leftDown = mouse;
59
67
  selection.start(mouse);
60
68
  requestRender();
61
69
  return { consume: true };
@@ -66,7 +74,14 @@ export function createMouseSelectionController({
66
74
  return { consume: true };
67
75
  }
68
76
  if (mouse?.type === "up") {
77
+ const wasClick = isSamePoint(leftDown, mouse);
78
+ leftDown = null;
69
79
  selection.finish(mouse, { clear: false });
80
+ if (wasClick && toggleOutputToolCard(mouse)) {
81
+ selection.clear();
82
+ requestRender();
83
+ return { consume: true };
84
+ }
70
85
  requestRender();
71
86
  return { consume: true };
72
87
  }
@@ -84,6 +99,10 @@ export function createMouseSelectionController({
84
99
  };
85
100
  }
86
101
 
102
+ function isSamePoint(a, b) {
103
+ return Boolean(a && b && a.row === b.row && a.col === b.col && a.button === b.button);
104
+ }
105
+
87
106
  function compactStatusMessage(message) {
88
107
  const text = String(message || "unknown error").replace(/\s+/g, " ").trim();
89
108
  return text.length > 80 ? `${text.slice(0, 79)}…` : text;
@@ -4,7 +4,7 @@ import { renderMarkdown } from "../markdown-renderer.mjs";
4
4
 
5
5
  export function appendSelectableEntries(entries, block, lines, width) {
6
6
  if (block.type !== "markdown") {
7
- for (const line of lines) entries.push({ line, source: null, codeSource: null, baseRow: entries.length });
7
+ for (const line of lines) entries.push({ line, source: null, codeSource: null, block, baseRow: entries.length });
8
8
  return;
9
9
  }
10
10
  const source = { kind: "markdown", text: block.text, startRow: entries.length, endRow: entries.length + lines.length - 1 };
@@ -18,10 +18,10 @@ export function appendSelectableEntries(entries, block, lines, width) {
18
18
  }
19
19
 
20
20
  export function sliceEntriesWithTail(baseEntries, tailLine, range) {
21
- if (!range) return tailLine == null ? baseEntries : [...baseEntries, { line: tailLine, source: null, codeSource: null, baseRow: baseEntries.length }];
21
+ if (!range) return tailLine == null ? baseEntries : [...baseEntries, { line: tailLine, source: null, codeSource: null, block: null, baseRow: baseEntries.length }];
22
22
  const { start, end } = range;
23
23
  const visible = baseEntries.slice(start, Math.min(end, baseEntries.length));
24
- if (tailLine != null && end > baseEntries.length) visible.push({ line: tailLine, source: null, codeSource: null, baseRow: baseEntries.length });
24
+ if (tailLine != null && end > baseEntries.length) visible.push({ line: tailLine, source: null, codeSource: null, block: null, baseRow: baseEntries.length });
25
25
  return visible;
26
26
  }
27
27
 
@@ -206,6 +206,15 @@ export class OutputBuffer {
206
206
  return changed;
207
207
  }
208
208
 
209
+ toggleToolCardAtVisibleRow(row, width) {
210
+ const entry = this._visibleEntryAt(row, width);
211
+ const block = entry?.block;
212
+ if (block?.type !== "tool-card") return false;
213
+ block.expanded = !block.expanded;
214
+ this._invalidateBaseLines();
215
+ return true;
216
+ }
217
+
209
218
  invalidate() { this._invalidateBaseLines(); }
210
219
 
211
220
  _invalidateBaseLines() {
@@ -232,6 +241,15 @@ export class OutputBuffer {
232
241
  return brightBlack(`${SPINNER_FRAMES[this.spinnerIdx]} ${this.spinnerText}`);
233
242
  }
234
243
 
244
+ _visibleEntryAt(row, width) {
245
+ const visibleRow = Math.trunc(row);
246
+ if (visibleRow < 0) return null;
247
+ const baseEntries = this._renderBaseEntries(width);
248
+ const tailLine = this.spinning ? this._spinnerLine() : null;
249
+ const entries = sliceEntriesWithTail(baseEntries, tailLine, this.scrollState.sliceRange());
250
+ return entries[visibleRow] ?? null;
251
+ }
252
+
235
253
  _renderBaseLines(width) {
236
254
  const cached = this._baseLinesCache.get(width);
237
255
  if (cached) return cached;
@@ -3,27 +3,49 @@ import { brightBlack } from "./ui-theme.mjs";
3
3
 
4
4
  const RECALL_ICON = "✦";
5
5
 
6
- export function formatRecallLines(hints = [], report = null) {
6
+ export function formatRecallLines(hints = [], report = null, { variant = "user" } = {}) {
7
+ if (variant === "assistant") return formatAssistantRecallLines(hints, report);
7
8
  const candidates = report?.candidates ?? [];
8
- if (!hints.length && !candidates.length) return [];
9
- const noun = hints.length === 1 ? "note" : "notes";
9
+ const displayed = candidates.length ? candidates : hints.map((hint) => ({ ...hint, recalled: true }));
10
+ if (!hints.length && !displayed.length) return [];
10
11
  const threshold = Number.isFinite(report?.threshold) ? ` · threshold ${formatScore(report.threshold)}` : "";
11
12
  const fallback = report?.vectorizerStatus === "fallback" ? " · fallback" : "";
12
13
  return [
13
- `${RECALL_ICON} Memory Recall · ${hints.length} ${noun}${threshold}${fallback}`,
14
+ `${RECALL_ICON} Memory Recall · ${recallSummary(hints, displayed)}${threshold}${fallback}`,
14
15
  ...(report?.warning ? [` ! ${report.warning}`] : []),
15
- ...(candidates.length ? candidates : hints.map((hint) => ({ ...hint, recalled: true }))).flatMap(formatHintLines),
16
+ ...displayed.flatMap(formatHintLines),
16
17
  ];
17
18
  }
18
19
 
19
- export function writeRecall({ output, hints = [], report = null }) {
20
- const lines = formatRecallLines(hints, report);
20
+ export function writeRecall({ output, hints = [], report = null, variant = "user" }) {
21
+ const lines = formatRecallLines(hints, report, { variant });
21
22
  lines.forEach((line) => {
22
- if (line.startsWith(" ")) output.writeln(brightBlack(line));
23
+ if (variant === "assistant" || line.startsWith(" ")) output.writeln(brightBlack(line));
23
24
  else output.writeln(line);
24
25
  });
25
26
  }
26
27
 
28
+ function formatAssistantRecallLines(hints, report) {
29
+ const candidates = (report?.candidates?.length ? report.candidates : hints.map((hint) => ({ ...hint, recalled: true }))).slice(0, 3);
30
+ const threshold = Number.isFinite(report?.threshold) ? ` · threshold ${formatScore(report.threshold)}` : "";
31
+ const fallback = report?.vectorizerStatus === "fallback" ? " · fallback" : "";
32
+ return [
33
+ `${RECALL_ICON} Memory Recall · ${recallSummary(hints, report?.candidates ?? candidates)}${threshold}${fallback}`,
34
+ ...(report?.warning ? [` ! ${report.warning}`] : []),
35
+ ...(candidates.length ? candidates.map(formatCompactHintLine) : [" no candidates"]),
36
+ ];
37
+ }
38
+
39
+ function recallSummary(hints, candidates) {
40
+ return `${hints.length} recalled · ${candidates.length} ${candidates.length === 1 ? "candidate" : "candidates"}`;
41
+ }
42
+
43
+ function formatCompactHintLine(hint) {
44
+ const title = hint.name || hint.id || "Untitled memory";
45
+ const mark = hint.recalled === false ? "×" : "✓";
46
+ return ` ${mark} ${formatScore(hint.score)} ${title}`;
47
+ }
48
+
27
49
  function formatHintLines(hint) {
28
50
  const title = hint.name || hint.id || "Untitled memory";
29
51
  const mark = hint.recalled === false ? "×" : "✓";
@@ -0,0 +1,88 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+
3
+ const INVERSE = "\x1b[7m";
4
+ const RESET = "\x1b[0m";
5
+
6
+ export function highlightAnsiLine(line, startCol, endCol) {
7
+ const { before, selected, after, activeAtStart, activeAtEnd } = splitAnsiColumns(line, startCol, endCol);
8
+ return `${before}${INVERSE}${activeAtStart}${keepInverseAfterReset(selected)}${RESET}${activeAtEnd}${after}`;
9
+ }
10
+
11
+ export function sliceColumns(text, startCol, endCol) {
12
+ let col = 0;
13
+ let result = "";
14
+ for (const ch of String(text ?? "")) {
15
+ const next = col + visibleWidth(ch);
16
+ if (next > startCol && col < endCol) result += ch;
17
+ col = next;
18
+ if (col >= endCol) break;
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function keepInverseAfterReset(text) {
24
+ return String(text ?? "").replace(/\x1b\[([0-9;]*)m/g, (seq, body) => {
25
+ const params = body === "" ? ["0"] : body.split(";");
26
+ return params.includes("0") ? `${seq}${INVERSE}` : seq;
27
+ });
28
+ }
29
+
30
+ function splitAnsiColumns(text, startCol, endCol) {
31
+ let col = 0;
32
+ let i = 0;
33
+ let before = "";
34
+ let selected = "";
35
+ let after = "";
36
+ let active = "";
37
+ let activeAtStart = "";
38
+ let activeAtEnd = "";
39
+ let capturedStart = false;
40
+ let capturedEnd = false;
41
+ const source = String(text ?? "");
42
+
43
+ while (i < source.length) {
44
+ const ansi = readAnsi(source, i);
45
+ if (ansi) {
46
+ active = updateActiveSgr(active, ansi);
47
+ if (col < startCol) before += ansi;
48
+ else if (col < endCol) selected += ansi;
49
+ else after += ansi;
50
+ i += ansi.length;
51
+ continue;
52
+ }
53
+
54
+ const ch = source[i];
55
+ if (!capturedStart && col >= startCol) {
56
+ activeAtStart = active;
57
+ capturedStart = true;
58
+ }
59
+ if (!capturedEnd && col >= endCol) {
60
+ activeAtEnd = active;
61
+ capturedEnd = true;
62
+ }
63
+
64
+ const next = col + visibleWidth(ch);
65
+ if (next <= startCol) before += ch;
66
+ else if (col >= endCol) after += ch;
67
+ else selected += ch;
68
+ col = next;
69
+ i += 1;
70
+ }
71
+
72
+ if (!capturedStart) activeAtStart = active;
73
+ if (!capturedEnd) activeAtEnd = active;
74
+ return { before, selected, after, activeAtStart, activeAtEnd };
75
+ }
76
+
77
+ function readAnsi(text, offset) {
78
+ if (text[offset] !== "\x1b") return null;
79
+ const match = text.slice(offset).match(/^\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/);
80
+ return match?.[0] ?? null;
81
+ }
82
+
83
+ function updateActiveSgr(active, seq) {
84
+ if (!seq.startsWith("\x1b[") || !seq.endsWith("m")) return active;
85
+ const body = seq.slice(2, -1);
86
+ if (body === "" || body.split(";").includes("0")) return "";
87
+ return seq;
88
+ }
@@ -1,8 +1,7 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { highlightAnsiLine, sliceColumns } from "./selection/ansi-range.mjs";
2
3
 
3
4
  const CONTROL_RE = /\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
4
- const INVERSE = "\x1b[7m";
5
- const RESET = "\x1b[0m";
6
5
 
7
6
  export class ScreenSelection {
8
7
  constructor() {
@@ -118,6 +117,12 @@ export class ScreenSelection {
118
117
  });
119
118
  }
120
119
 
120
+ hitTest(point) {
121
+ const hit = hitRegion(point, this.regions);
122
+ if (!hit) return null;
123
+ return { regionId: hit.region.id, row: hit.localRow, col: hit.localCol };
124
+ }
125
+
121
126
  _plainLine(row) {
122
127
  if (!this._plainLines.has(row)) this._plainLines.set(row, stripAnsi(this.lines[row] ?? ""));
123
128
  return this._plainLines.get(row);
@@ -167,24 +172,19 @@ function localRange(range, region) {
167
172
  }
168
173
 
169
174
  function normalizePoint({ row, col }, regions, clamp) {
175
+ const hit = hitRegion({ row, col }, regions);
176
+ if (hit) {
177
+ const maxCol = Number.isFinite(hit.region.width) ? hit.region.width : Infinity;
178
+ return {
179
+ row: hit.region.docStart + hit.localRow,
180
+ col: clampNumber(hit.localCol, 0, maxCol),
181
+ };
182
+ }
183
+ if (!clamp) return null;
184
+
170
185
  const screenRow = Math.trunc(row) - 1;
171
- const screenCol = Math.trunc(col) - 1;
172
186
  if (regions.length === 0) return null;
173
187
 
174
- for (const region of regions) {
175
- const localRow = screenRow - region.topRow;
176
- const localCol = screenCol - region.leftCol;
177
- const maxCol = Number.isFinite(region.width) ? region.width : Infinity;
178
- if (localRow >= 0 && localRow < region.lines.length) {
179
- if (!clamp && (localCol < 0 || localCol > maxCol)) return null;
180
- return {
181
- row: region.docStart + localRow,
182
- col: clampNumber(localCol, 0, maxCol),
183
- };
184
- }
185
- }
186
-
187
- if (!clamp) return null;
188
188
  const first = regions[0];
189
189
  const last = regions.at(-1);
190
190
  if (screenRow < first.topRow) return { row: first.docStart, col: 0 };
@@ -205,94 +205,26 @@ function normalizePoint({ row, col }, regions, clamp) {
205
205
  return nearest ? { row: nearest.row, col: nearest.col } : null;
206
206
  }
207
207
 
208
- function comparePoints(a, b) {
209
- if (a.row !== b.row) return a.row - b.row;
210
- return a.col - b.col;
211
- }
212
-
213
- function highlightAnsiLine(line, startCol, endCol) {
214
- const { before, selected, after, activeAtStart, activeAtEnd } = splitAnsiColumns(line, startCol, endCol);
215
- return `${before}${INVERSE}${activeAtStart}${keepInverseAfterReset(selected)}${RESET}${activeAtEnd}${after}`;
216
- }
217
-
218
- function keepInverseAfterReset(text) {
219
- return String(text ?? "").replace(/\x1b\[([0-9;]*)m/g, (seq, body) => {
220
- const params = body === "" ? ["0"] : body.split(";");
221
- return params.includes("0") ? `${seq}${INVERSE}` : seq;
222
- });
223
- }
224
-
225
- function sliceColumns(text, startCol, endCol) {
226
- let col = 0;
227
- let result = "";
228
- for (const ch of String(text ?? "")) {
229
- const next = col + visibleWidth(ch);
230
- if (next > startCol && col < endCol) result += ch;
231
- col = next;
232
- if (col >= endCol) break;
208
+ function hitRegion({ row, col }, regions) {
209
+ const screenRow = Math.trunc(row) - 1;
210
+ const screenCol = Math.trunc(col) - 1;
211
+ for (const region of regions) {
212
+ const localRow = screenRow - region.topRow;
213
+ const localCol = screenCol - region.leftCol;
214
+ const maxCol = Number.isFinite(region.width) ? region.width : Infinity;
215
+ if (localRow < 0 || localRow >= region.lines.length) continue;
216
+ if (localCol < 0 || localCol > maxCol) continue;
217
+ return { region, localRow, localCol };
233
218
  }
234
- return result;
219
+ return null;
235
220
  }
236
221
 
237
- function splitAnsiColumns(text, startCol, endCol) {
238
- let col = 0;
239
- let i = 0;
240
- let before = "";
241
- let selected = "";
242
- let after = "";
243
- let active = "";
244
- let activeAtStart = "";
245
- let activeAtEnd = "";
246
- let capturedStart = false;
247
- let capturedEnd = false;
248
- const source = String(text ?? "");
249
-
250
- while (i < source.length) {
251
- const ansi = readAnsi(source, i);
252
- if (ansi) {
253
- active = updateActiveSgr(active, ansi);
254
- if (col < startCol) before += ansi;
255
- else if (col < endCol) selected += ansi;
256
- else after += ansi;
257
- i += ansi.length;
258
- continue;
259
- }
260
-
261
- const ch = source[i];
262
- if (!capturedStart && col >= startCol) {
263
- activeAtStart = active;
264
- capturedStart = true;
265
- }
266
- if (!capturedEnd && col >= endCol) {
267
- activeAtEnd = active;
268
- capturedEnd = true;
269
- }
270
-
271
- const next = col + visibleWidth(ch);
272
- if (next <= startCol) before += ch;
273
- else if (col >= endCol) after += ch;
274
- else selected += ch;
275
- col = next;
276
- i += 1;
277
- }
278
-
279
- if (!capturedStart) activeAtStart = active;
280
- if (!capturedEnd) activeAtEnd = active;
281
- return { before, selected, after, activeAtStart, activeAtEnd };
222
+ function comparePoints(a, b) {
223
+ if (a.row !== b.row) return a.row - b.row;
224
+ return a.col - b.col;
282
225
  }
283
226
 
284
- function readAnsi(text, offset) {
285
- if (text[offset] !== "\x1b") return null;
286
- const match = text.slice(offset).match(/^\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/);
287
- return match?.[0] ?? null;
288
- }
289
227
 
290
- function updateActiveSgr(active, seq) {
291
- if (!seq.startsWith("\x1b[") || !seq.endsWith("m")) return active;
292
- const body = seq.slice(2, -1);
293
- if (body === "" || body.split(";").includes("0")) return "";
294
- return seq;
295
- }
296
228
 
297
229
  function clampNumber(value, min, max) {
298
230
  return Math.min(max, Math.max(min, value));
@@ -7,7 +7,8 @@ import { formatShellHints } from "../../shell/hints.mjs";
7
7
  export async function prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState = null }) {
8
8
  const engine = runner.engine ?? {};
9
9
  const carryoverAlreadyRendered = engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
10
- const carryoverRecallHints = engine.takePendingAssistantRecallHints?.() ?? [];
10
+ const carryoverRecall = normalizePendingAssistantRecall(engine.takePendingAssistantRecallHints?.());
11
+ const carryoverRecallHints = carryoverRecall.hints;
11
12
  const userRecallHints = await memoryStore.recallForUser(prompt, {
12
13
  currentProject,
13
14
  excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
@@ -29,7 +30,8 @@ export async function prepareTurnInput({ prompt, runner, memoryStore, currentPro
29
30
  userRecallHints,
30
31
  userRecallReport,
31
32
  carryoverRecallHints,
32
- shouldRenderCarryoverRecall: carryoverRecallHints.length > 0 && !carryoverAlreadyRendered,
33
+ carryoverRecallReport: carryoverRecall.report,
34
+ shouldRenderCarryoverRecall: (carryoverRecallHints.length > 0 || carryoverRecall.report) && !carryoverAlreadyRendered,
33
35
  };
34
36
  }
35
37
 
@@ -40,3 +42,8 @@ export function formatUserDisplayMessage(prompt) {
40
42
  function appendPromptBlocks(prompt, ...blocks) {
41
43
  return [prompt, ...blocks.filter(Boolean)].join("\n\n");
42
44
  }
45
+
46
+ function normalizePendingAssistantRecall(value) {
47
+ if (Array.isArray(value)) return { hints: value, report: null };
48
+ return { hints: value?.hints ?? [], report: value?.report ?? null };
49
+ }
package/src/cli/ui.mjs CHANGED
@@ -212,8 +212,8 @@ export function createTuiUI({
212
212
  status: (text) => {
213
213
  ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
214
214
  },
215
- recall: ({ hints, report }) => {
216
- ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeRecall({ output, hints, report }); requestRender();
215
+ recall: ({ hints, report, variant }) => {
216
+ ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeRecall({ output, hints, report, variant }); requestRender();
217
217
  },
218
218
 
219
219
  clearOutput: () => {
@@ -17,6 +17,7 @@ export class ContextEngine {
17
17
  this.thinkingLevel = thinkingLevel;
18
18
  this.turns = [];
19
19
  this.pendingAssistantRecallHints = [];
20
+ this.pendingAssistantRecallReport = null;
20
21
  this.pendingAssistantRecallHintsRendered = false;
21
22
  this.sessionName = "";
22
23
  this.toolDefs = [];
@@ -96,8 +97,9 @@ export class ContextEngine {
96
97
  return ids;
97
98
  }
98
99
 
99
- setPendingAssistantRecallHints(hints = []) {
100
+ setPendingAssistantRecallHints(hints = [], report = null) {
100
101
  this.pendingAssistantRecallHints = uniqueHints(hints);
102
+ this.pendingAssistantRecallReport = report;
101
103
  this.pendingAssistantRecallHintsRendered = false;
102
104
  }
103
105
 
@@ -105,19 +107,25 @@ export class ContextEngine {
105
107
  return this.pendingAssistantRecallHints;
106
108
  }
107
109
 
110
+ peekPendingAssistantRecallReport() {
111
+ return this.pendingAssistantRecallReport;
112
+ }
113
+
108
114
  hasRenderedPendingAssistantRecallHints() {
109
115
  return this.pendingAssistantRecallHintsRendered;
110
116
  }
111
117
 
112
118
  markPendingAssistantRecallHintsRendered() {
113
- if (this.pendingAssistantRecallHints.length > 0) this.pendingAssistantRecallHintsRendered = true;
119
+ if (this.pendingAssistantRecallHints.length > 0 || this.pendingAssistantRecallReport) this.pendingAssistantRecallHintsRendered = true;
114
120
  }
115
121
 
116
122
  takePendingAssistantRecallHints() {
117
123
  const hints = this.pendingAssistantRecallHints;
124
+ const report = this.pendingAssistantRecallReport;
118
125
  this.pendingAssistantRecallHints = [];
126
+ this.pendingAssistantRecallReport = null;
119
127
  this.pendingAssistantRecallHintsRendered = false;
120
- return hints;
128
+ return { hints, report };
121
129
  }
122
130
 
123
131
  resolvePath(raw) {
@@ -142,12 +150,14 @@ export class ContextEngine {
142
150
  if (replace) {
143
151
  this.turns = [];
144
152
  this.pendingAssistantRecallHints = [];
153
+ this.pendingAssistantRecallReport = null;
145
154
  this.pendingAssistantRecallHintsRendered = false;
146
155
  this.sessionName = "";
147
156
  }
148
157
  if (data.turns) this.turns = data.turns;
149
158
  if (Array.isArray(data.pendingAssistantRecallHints)) {
150
159
  this.pendingAssistantRecallHints = uniqueHints(data.pendingAssistantRecallHints);
160
+ this.pendingAssistantRecallReport = data.pendingAssistantRecallReport ?? null;
151
161
  this.pendingAssistantRecallHintsRendered = false;
152
162
  }
153
163
  if (typeof data.sessionName === "string") this.sessionName = data.sessionName;
@@ -15,3 +15,12 @@ export async function preloadSemanticMemoryRecall({ memoryStore, ui = null, logg
15
15
  return { ok: false, error: message };
16
16
  }
17
17
  }
18
+
19
+ export function startSemanticMemoryRecallPreload({ memoryStore, ui = null, logger = null, delayMs = 0 } = {}) {
20
+ if (!memoryStore?.semanticRecall?.enabled) return null;
21
+ const start = () => preloadSemanticMemoryRecall({ memoryStore, ui, logger });
22
+ if (delayMs <= 0) return start();
23
+ const timer = setTimeout(start, delayMs);
24
+ timer.unref?.();
25
+ return timer;
26
+ }
@@ -7,7 +7,7 @@ import { parseMemoryMarkdown } from "./markdown-format.mjs";
7
7
  export const POTION_RETRIEVAL_MODEL_ID = "minishlab/potion-retrieval-32M";
8
8
 
9
9
  const MAX_CHUNK_CHARS = 1800;
10
- export const DEFAULT_MEMORY_RECALL_MIN_SCORE = 0.3;
10
+ export const DEFAULT_MEMORY_RECALL_MIN_SCORE = 0.5;
11
11
 
12
12
  export class SemanticMemoryRecallIndex {
13
13
  constructor({ stateRoot = null, modelId = POTION_RETRIEVAL_MODEL_ID, modelDir = null, vectorizer = null, minScore = parseMemoryRecallMinScore() } = {}) {
@@ -56,13 +56,17 @@ export class SemanticMemoryRecallIndex {
56
56
  if (!prev || score > prev.score) bestByEntry.set(chunk.entry.id, { entry: chunk.entry, score });
57
57
  }
58
58
 
59
- const candidates = [...bestByEntry.values()]
59
+ const ranked = [...bestByEntry.values()]
60
60
  .filter(({ score }) => Number.isFinite(score) && score > 0)
61
- .sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name))
62
- .map(({ entry, score }) => ({ entry, score, recalled: score >= this.minScore }));
61
+ .sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name));
62
+ const recalled = ranked.filter(({ score }) => score >= this.minScore).slice(0, limit);
63
+ const recalledIds = new Set(recalled.map(({ entry }) => entry.id));
64
+ const candidates = ranked
65
+ .slice(0, Math.max(limit, candidateLimit))
66
+ .map(({ entry, score }) => ({ entry, score, recalled: recalledIds.has(entry.id) }));
63
67
  return {
64
- recalled: candidates.filter((candidate) => candidate.recalled).slice(0, limit),
65
- candidates: candidates.slice(0, Math.max(limit, candidateLimit)),
68
+ recalled,
69
+ candidates,
66
70
  threshold: this.minScore,
67
71
  vectorizerStatus: this.status,
68
72
  warning: this.warning,