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.
- package/package.json +1 -1
- package/src/agent/code-search/cache.mjs +13 -5
- package/src/agent/code-search/engine.mjs +7 -2
- package/src/agent/code-search/retrieval/safetensors.mjs +16 -10
- package/src/agent/code-search/scanner.mjs +11 -5
- package/src/agent/model-payload-dumper.mjs +1 -1
- package/src/agent/runner/payload/provider-payload-transform.mjs +59 -0
- package/src/agent/runner/recall/mid-turn-recall-bridge.mjs +23 -0
- package/src/agent/runner.mjs +28 -27
- package/src/agent/runtime/remote-ui-client.mjs +1 -1
- package/src/agent/runtime/resource/context-resource-loader.mjs +17 -0
- package/src/agent/runtime/runtime-factory.mjs +5 -1
- package/src/agent/runtime/state/runner-state.mjs +10 -3
- package/src/agent/runtime/ui-event-bridge.mjs +2 -2
- package/src/agent/turn/turn-runner.mjs +20 -16
- package/src/cli/fallback-ui.mjs +2 -2
- package/src/cli/repl-loop.mjs +5 -4
- package/src/cli/startup/app-runtime.mjs +2 -3
- package/src/cli/tui/input/mouse-selection-controller.mjs +19 -0
- package/src/cli/tui/output/selectable-copy.mjs +3 -3
- package/src/cli/tui/output-buffer.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +30 -8
- package/src/cli/tui/selection/ansi-range.mjs +88 -0
- package/src/cli/tui/selection-screen.mjs +31 -99
- package/src/cli/turn/turn-input-preparer.mjs +9 -2
- package/src/cli/ui.mjs +2 -2
- package/src/context/engine.mjs +13 -3
- package/src/memory/markdown/semantic-preload.mjs +9 -0
- package/src/memory/markdown/semantic-recall.mjs +10 -6
- package/src/memory/markdown-store.mjs +19 -11
- package/src/web-ui/dist/assets/{index-CcbYCcWs.css → index-BG1Pxf1k.css} +1 -1
- package/src/web-ui/dist/assets/{index-CBYbNVgs.js → index-C0xOHlDz.js} +1 -1
- package/src/web-ui/dist/index.html +2 -2
- package/src/web-ui/runtime-host.mjs +15 -3
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +9 -6
- package/src/web-ui/src/model.ts +2 -2
- package/src/web-ui/src/runtime/client.ts +1 -1
- package/src/web-ui/src/runtime/runtimeTimeline.ts +2 -2
- package/src/web-ui/src/styles/shell.css +1 -0
- package/src/web-ui/src/timelineAdapter.ts +1 -1
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -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
|
-
|
|
219
|
-
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
14
|
+
`${RECALL_ICON} Memory Recall · ${recallSummary(hints, displayed)}${threshold}${fallback}`,
|
|
14
15
|
...(report?.warning ? [` ! ${report.warning}`] : []),
|
|
15
|
-
...
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
219
|
+
return null;
|
|
235
220
|
}
|
|
236
221
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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
|
-
|
|
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: () => {
|
package/src/context/engine.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
|
65
|
-
candidates
|
|
68
|
+
recalled,
|
|
69
|
+
candidates,
|
|
66
70
|
threshold: this.minScore,
|
|
67
71
|
vectorizerStatus: this.status,
|
|
68
72
|
warning: this.warning,
|