march-cli 0.1.42 → 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 +16 -7
- package/src/agent/code-search/engine.mjs +9 -2
- package/src/agent/code-search/retrieval/resilient-vectorizer.mjs +59 -0
- package/src/agent/code-search/retrieval/safetensors.mjs +16 -10
- package/src/agent/code-search/scanner.mjs +11 -5
- package/src/agent/code-search/tool.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 +35 -24
- package/src/cli/fallback-ui.mjs +2 -2
- package/src/cli/repl-loop.mjs +9 -8
- package/src/cli/startup/app-runtime.mjs +4 -2
- 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/timeline-block-restore.mjs +1 -1
- package/src/cli/tui/output-buffer.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +38 -9
- 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 +15 -6
- package/src/cli/ui.mjs +2 -2
- package/src/cli/workspace/tui-timeline-projection.mjs +1 -1
- package/src/context/engine.mjs +15 -5
- package/src/context/system-core/base.md +1 -1
- package/src/memory/markdown/markdown-format.mjs +0 -17
- package/src/memory/markdown/markdown-recall.mjs +11 -19
- package/src/memory/markdown/semantic-preload.mjs +26 -0
- package/src/memory/markdown/semantic-recall.mjs +169 -0
- package/src/memory/markdown/sqlite-index.mjs +1 -13
- package/src/memory/markdown-store.mjs +34 -54
- package/src/web-ui/dist/assets/{index-BQtl1uQs.css → index-BG1Pxf1k.css} +1 -1
- package/src/web-ui/dist/assets/{index-DrlJis_D.js → index-C0xOHlDz.js} +1 -1
- package/src/web-ui/dist/index.html +2 -2
- package/src/web-ui/runtime-host.mjs +18 -3
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +27 -0
- package/src/web-ui/src/model.ts +18 -0
- package/src/web-ui/src/runtime/client.ts +2 -1
- package/src/web-ui/src/runtime/runtimeTimeline.ts +5 -0
- package/src/web-ui/src/styles/shell.css +7 -0
- package/src/web-ui/src/timelineAdapter.ts +2 -0
|
@@ -50,7 +50,7 @@ export function createRuntimeUiClient(eventBus) {
|
|
|
50
50
|
retryEnd: (event) => eventBus.emit({ type: "retry_end", ...event }),
|
|
51
51
|
status: (text) => eventBus.emit({ type: "status", text }),
|
|
52
52
|
debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
|
|
53
|
-
recall: ({
|
|
53
|
+
recall: ({ hints, report, variant }) => eventBus.emit({ type: "recall", hints, report, variant }),
|
|
54
54
|
providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
|
|
55
55
|
editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
|
|
56
56
|
};
|
|
@@ -71,7 +71,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
|
|
|
71
71
|
case "retry_end": return ui.retryEnd?.(pickRetryEnd(event));
|
|
72
72
|
case "status": return ui.status?.(event.text);
|
|
73
73
|
case "debug_lines": return writeDebugLines(ui, event.lines);
|
|
74
|
-
case "recall": return ui.recall?.({
|
|
74
|
+
case "recall": return ui.recall?.({ hints: event.hints, report: event.report, variant: event.variant });
|
|
75
75
|
case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
|
|
76
76
|
case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
|
|
77
77
|
default: return undefined;
|
|
@@ -20,14 +20,15 @@ export async function runRunnerTurn({
|
|
|
20
20
|
autoNameSession,
|
|
21
21
|
contextMode = "rebuild",
|
|
22
22
|
recordHistory = null,
|
|
23
|
+
trackMidTurnRecallInjection = null,
|
|
23
24
|
}) {
|
|
24
25
|
const {
|
|
25
26
|
userRecallHints = [],
|
|
26
|
-
currentProject = "",
|
|
27
27
|
} = options;
|
|
28
28
|
const activeSession = sessionBinding.get();
|
|
29
29
|
const turnState = createTurnEventState();
|
|
30
30
|
const midTurnRecallHints = [];
|
|
31
|
+
const midTurnRecallTasks = [];
|
|
31
32
|
const idleWatchdog = createModelStreamIdleWatchdog({ session: activeSession, logger, setPhase });
|
|
32
33
|
ui.turnStart();
|
|
33
34
|
setPhase?.("subscribed");
|
|
@@ -57,12 +58,9 @@ export async function runRunnerTurn({
|
|
|
57
58
|
}
|
|
58
59
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
59
60
|
if (event.type === "tool_execution_start") {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
queueMidTurnRecallHints(activeSession, hints, logger);
|
|
64
|
-
ui.recall?.({ source: "assistant", hints });
|
|
65
|
-
}
|
|
61
|
+
const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints, trackMidTurnRecallInjection });
|
|
62
|
+
trackMidTurnRecallInjection?.({ task });
|
|
63
|
+
midTurnRecallTasks.push(task);
|
|
66
64
|
}
|
|
67
65
|
});
|
|
68
66
|
|
|
@@ -90,11 +88,11 @@ export async function runRunnerTurn({
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
setPhase?.("finalizing");
|
|
93
|
-
|
|
91
|
+
await Promise.allSettled(midTurnRecallTasks);
|
|
92
|
+
await finalizeTurn({
|
|
94
93
|
prompt,
|
|
95
94
|
userMessage,
|
|
96
95
|
userRecallHints,
|
|
97
|
-
currentProject,
|
|
98
96
|
memoryStore,
|
|
99
97
|
engine,
|
|
100
98
|
ui,
|
|
@@ -172,17 +170,17 @@ function throwIfAssistantEndedWithError(turnState) {
|
|
|
172
170
|
}
|
|
173
171
|
|
|
174
172
|
function queueMidTurnRecallHints(session, hints, logger) {
|
|
175
|
-
const content = formatRecallHints(
|
|
176
|
-
if (!content) return;
|
|
177
|
-
const
|
|
173
|
+
const content = formatRecallHints(hints);
|
|
174
|
+
if (!content) return null;
|
|
175
|
+
const task = Promise.resolve(session.sendCustomMessage?.({
|
|
178
176
|
customType: "march.recall",
|
|
179
177
|
content,
|
|
180
178
|
display: false,
|
|
181
|
-
details: {
|
|
182
|
-
}, { deliverAs: "steer" })
|
|
183
|
-
void injected?.catch?.((err) => {
|
|
179
|
+
details: { type: "recall" },
|
|
180
|
+
}, { deliverAs: "steer" })).catch((err) => {
|
|
184
181
|
logger?.debug("memory.mid_turn_recall.inject_failed", { errorMessage: err?.message ?? String(err) });
|
|
185
182
|
});
|
|
183
|
+
return { content, task };
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
function logSessionEvent(logger, event) {
|
|
@@ -209,11 +207,11 @@ function logSessionEvent(logger, event) {
|
|
|
209
207
|
});
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
function finalizeTurn({ prompt, userMessage, userRecallHints,
|
|
210
|
+
async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
|
|
213
211
|
closeAssistantReply({ ui, state: turnState });
|
|
214
|
-
const
|
|
215
|
-
engine.setPendingAssistantRecallHints?.(
|
|
216
|
-
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...
|
|
212
|
+
const assistantRecall = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
213
|
+
engine.setPendingAssistantRecallHints?.(assistantRecall.hints, assistantRecall.report);
|
|
214
|
+
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecall.hints]);
|
|
217
215
|
|
|
218
216
|
const turn = engine.recordTurn({
|
|
219
217
|
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
@@ -228,17 +226,30 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
|
|
|
228
226
|
syncCurrentMarchSessionState();
|
|
229
227
|
}
|
|
230
228
|
|
|
231
|
-
function flushAssistantRecall({ memoryStore, engine, turnState
|
|
232
|
-
if (!memoryStore) return [];
|
|
229
|
+
async function flushAssistantRecall({ memoryStore, engine, turnState }) {
|
|
230
|
+
if (!memoryStore) return { hints: [], report: null };
|
|
233
231
|
const text = assistantRecallDeltaText(turnState);
|
|
234
232
|
advanceAssistantRecallCursor(turnState);
|
|
235
|
-
if (!text.trim()) return [];
|
|
236
|
-
return memoryStore.recallForAssistant(text, {
|
|
237
|
-
currentProject,
|
|
233
|
+
if (!text.trim()) return { hints: [], report: null };
|
|
234
|
+
return await memoryStore.recallForAssistant(text, {
|
|
238
235
|
excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
|
|
239
236
|
});
|
|
240
237
|
}
|
|
241
238
|
|
|
239
|
+
async function flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints, trackMidTurnRecallInjection }) {
|
|
240
|
+
try {
|
|
241
|
+
const { hints, report } = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
242
|
+
if (hints.length > 0) {
|
|
243
|
+
midTurnRecallHints.push(...hints);
|
|
244
|
+
const injection = queueMidTurnRecallHints(activeSession, hints, logger);
|
|
245
|
+
if (injection) trackMidTurnRecallInjection?.(injection);
|
|
246
|
+
}
|
|
247
|
+
if (report) ui.recall?.({ hints, report, variant: "assistant" });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
logger?.debug("memory.mid_turn_recall.failed", { errorMessage: err?.message ?? String(err) });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
242
253
|
function assistantRecallDeltaText(turnState) {
|
|
243
254
|
const cursor = turnState.recallCursor ?? { draftLength: 0, thinkingLength: 0 };
|
|
244
255
|
const thinking = assistantThinkingText(turnState);
|
package/src/cli/fallback-ui.mjs
CHANGED
|
@@ -109,9 +109,9 @@ export function createPlainUI() {
|
|
|
109
109
|
},
|
|
110
110
|
textDelta: writeText,
|
|
111
111
|
status: (text) => { ensureNewline(); stdout.write(`${brightBlack(`● ${text}`)}\n`); },
|
|
112
|
-
recall: ({ hints }) => {
|
|
112
|
+
recall: ({ hints, report, variant }) => {
|
|
113
113
|
ensureNewline();
|
|
114
|
-
for (const line of formatRecallLines(hints)) stdout.write(`${brightBlack(line)}\n`);
|
|
114
|
+
for (const line of formatRecallLines(hints, report, { variant })) stdout.write(`${brightBlack(line)}\n`);
|
|
115
115
|
},
|
|
116
116
|
clearOutput: () => {},
|
|
117
117
|
restoreTranscript: () => {},
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -16,10 +16,10 @@ export async function runSingleShotPrompt({
|
|
|
16
16
|
}) {
|
|
17
17
|
memoryStore.beginTurn();
|
|
18
18
|
try {
|
|
19
|
-
const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
19
|
+
const turnInput = await prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
20
20
|
ui.writeln(turnInput.displayMessage);
|
|
21
|
-
ui.recall?.({
|
|
22
|
-
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({
|
|
21
|
+
ui.recall?.({ hints: turnInput.userRecallHints, report: turnInput.userRecallReport });
|
|
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 });
|
|
@@ -185,10 +185,10 @@ function startReplTurn({ runtime, prompt, ui, refreshStatusBar, setTurnRunning,
|
|
|
185
185
|
async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
|
|
186
186
|
memoryStore.beginTurn();
|
|
187
187
|
try {
|
|
188
|
-
const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
188
|
+
const turnInput = await prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
189
189
|
ui.writeln(turnInput.displayMessage);
|
|
190
|
-
ui.recall?.({
|
|
191
|
-
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({
|
|
190
|
+
ui.recall?.({ hints: turnInput.userRecallHints, report: turnInput.userRecallReport });
|
|
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,6 +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 { startSemanticMemoryRecallPreload } from "../../memory/markdown/semantic-preload.mjs";
|
|
14
15
|
import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
|
|
15
16
|
import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
|
|
16
17
|
import { loadOrCreateProjectId, resumeStartupSession } from "./startup-session.mjs";
|
|
@@ -68,7 +69,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
68
69
|
const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
|
|
69
70
|
const profilePaths = defaultProfilePaths();
|
|
70
71
|
ensureProfileFiles(profilePaths);
|
|
71
|
-
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
|
|
72
|
+
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot, stateRoot });
|
|
72
73
|
const remoteMemorySources = normalizeRemoteMemorySources(config);
|
|
73
74
|
const currentProject = basename(cwd);
|
|
74
75
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
@@ -140,6 +141,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
140
141
|
return { ok: false, code: 1, logger };
|
|
141
142
|
}
|
|
142
143
|
syncRuntimeSessionStateFromRunner(sessionState, runner, sessionsRoot);
|
|
144
|
+
startSemanticMemoryRecallPreload({ memoryStore, logger, delayMs: 1000 });
|
|
143
145
|
|
|
144
146
|
const initialRuntime = {
|
|
145
147
|
project: currentProjectInfo,
|
|
@@ -166,7 +168,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
166
168
|
stateRoot,
|
|
167
169
|
memoryRoot,
|
|
168
170
|
profilePaths,
|
|
169
|
-
createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot }),
|
|
171
|
+
createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot, stateRoot }),
|
|
170
172
|
provider,
|
|
171
173
|
serviceTier,
|
|
172
174
|
model,
|
|
@@ -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
|
|
|
@@ -30,7 +30,7 @@ function appendTimelineBlock(output, block) {
|
|
|
30
30
|
output.addBlock({ type: "status", lines: [String(block.content ?? "")] });
|
|
31
31
|
break;
|
|
32
32
|
case "recall":
|
|
33
|
-
output.addBlock({ type: "plain", lines: formatRecallLines(block.hints ?? []) });
|
|
33
|
+
output.addBlock({ type: "plain", lines: formatRecallLines(block.hints ?? [], block.report ?? null) });
|
|
34
34
|
break;
|
|
35
35
|
case "editDiff":
|
|
36
36
|
output.addBlock({ type: "diff", path: block.path, diffLines: block.diffLines ?? [] });
|
|
@@ -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;
|
|
@@ -1,27 +1,56 @@
|
|
|
1
|
+
import { formatScore } from "../../memory/markdown/markdown-recall.mjs";
|
|
1
2
|
import { brightBlack } from "./ui-theme.mjs";
|
|
2
3
|
|
|
3
4
|
const RECALL_ICON = "✦";
|
|
4
5
|
|
|
5
|
-
export function formatRecallLines(hints = []) {
|
|
6
|
-
if (
|
|
7
|
-
const
|
|
6
|
+
export function formatRecallLines(hints = [], report = null, { variant = "user" } = {}) {
|
|
7
|
+
if (variant === "assistant") return formatAssistantRecallLines(hints, report);
|
|
8
|
+
const candidates = report?.candidates ?? [];
|
|
9
|
+
const displayed = candidates.length ? candidates : hints.map((hint) => ({ ...hint, recalled: true }));
|
|
10
|
+
if (!hints.length && !displayed.length) return [];
|
|
11
|
+
const threshold = Number.isFinite(report?.threshold) ? ` · threshold ${formatScore(report.threshold)}` : "";
|
|
12
|
+
const fallback = report?.vectorizerStatus === "fallback" ? " · fallback" : "";
|
|
8
13
|
return [
|
|
9
|
-
`${RECALL_ICON} Memory Recall · ${hints
|
|
10
|
-
...
|
|
14
|
+
`${RECALL_ICON} Memory Recall · ${recallSummary(hints, displayed)}${threshold}${fallback}`,
|
|
15
|
+
...(report?.warning ? [` ! ${report.warning}`] : []),
|
|
16
|
+
...displayed.flatMap(formatHintLines),
|
|
11
17
|
];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
export function writeRecall({ output, hints = [] }) {
|
|
15
|
-
const lines = formatRecallLines(hints);
|
|
20
|
+
export function writeRecall({ output, hints = [], report = null, variant = "user" }) {
|
|
21
|
+
const lines = formatRecallLines(hints, report, { variant });
|
|
16
22
|
lines.forEach((line) => {
|
|
17
|
-
if (line.startsWith(" ")) output.writeln(brightBlack(line));
|
|
23
|
+
if (variant === "assistant" || line.startsWith(" ")) output.writeln(brightBlack(line));
|
|
18
24
|
else output.writeln(line);
|
|
19
25
|
});
|
|
20
26
|
}
|
|
21
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
|
+
|
|
22
49
|
function formatHintLines(hint) {
|
|
23
50
|
const title = hint.name || hint.id || "Untitled memory";
|
|
24
|
-
const
|
|
51
|
+
const mark = hint.recalled === false ? "×" : "✓";
|
|
52
|
+
const score = `${formatScore(hint.score)} `;
|
|
53
|
+
const lines = [` ${mark} ${score}${title}`];
|
|
25
54
|
if (hint.description) lines.push(` ${hint.description}`);
|
|
26
55
|
return lines;
|
|
27
56
|
}
|
|
@@ -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));
|