march-cli 0.1.39 → 0.1.40
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/cli/input/keybindings.mjs +2 -0
- package/src/cli/startup/app-runtime.mjs +3 -7
- package/src/cli/tui/output/timeline-block-restore.mjs +45 -0
- package/src/cli/tui/output-buffer.mjs +5 -0
- package/src/cli/tui/tui-input-controller.mjs +16 -0
- package/src/cli/ui.mjs +6 -2
- package/src/cli/workspace/output-router.mjs +40 -33
- package/src/cli/workspace/tui-timeline-projection.mjs +179 -0
- package/src/cli/workspace/tui-timeline.mjs +247 -0
- package/src/session/state/march-session-state.mjs +4 -14
- package/src/session/state/march-session-ui-state.mjs +48 -19
- package/src/workspace/supervisor.mjs +5 -5
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ export const DEFAULT_KEYBINDINGS = Object.freeze({
|
|
|
9
9
|
thinkingSelector: "Ctrl+T",
|
|
10
10
|
modelSelector: "Ctrl+L",
|
|
11
11
|
externalEditor: "Ctrl+G",
|
|
12
|
+
clearInput: "Ctrl+U",
|
|
12
13
|
toggleToolOutput: "Ctrl+O",
|
|
13
14
|
toggleShellDrawer: "Alt+S",
|
|
14
15
|
nextShell: "Alt+N",
|
|
@@ -27,6 +28,7 @@ export const KEYBINDING_ACTIONS = Object.freeze({
|
|
|
27
28
|
thinkingSelector: "Open thinking selector",
|
|
28
29
|
modelSelector: "Open model selector",
|
|
29
30
|
externalEditor: "Open external editor ($VISUAL or $EDITOR)",
|
|
31
|
+
clearInput: "Clear current input draft",
|
|
30
32
|
toggleToolOutput: "Toggle tool output collapsed/expanded",
|
|
31
33
|
toggleShellDrawer: "Toggle right-side shell pane",
|
|
32
34
|
nextShell: "Select next shell in pane",
|
|
@@ -94,7 +94,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
94
94
|
ui,
|
|
95
95
|
activeProjectId: currentProjectInfo.projectId,
|
|
96
96
|
activeSessionId: sessionState.sessionId,
|
|
97
|
-
|
|
97
|
+
onPersistRenderTimeline: persistRenderTimeline,
|
|
98
98
|
});
|
|
99
99
|
const runtimeUi = outputRouter.createProjectUi(currentProjectInfo.projectId, () => sessionState.sessionId);
|
|
100
100
|
let turnRunning = false;
|
|
@@ -213,8 +213,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
213
213
|
outputRouter.setActiveSession(currentProjectInfo.projectId, sessionState.sessionId, { renderTimeline: loadStoredRenderTimeline(projectMarchDir, sessionState.sessionId) });
|
|
214
214
|
refreshStatusBar();
|
|
215
215
|
|
|
216
|
-
function persistRenderTimeline({ projectId, sessionId, events
|
|
217
|
-
if (!sessionId
|
|
216
|
+
function persistRenderTimeline({ projectId, sessionId, events }) {
|
|
217
|
+
if (!sessionId) return;
|
|
218
218
|
const routeProjectMarchDir = projectMarchDirs.get(projectId);
|
|
219
219
|
if (!routeProjectMarchDir) return;
|
|
220
220
|
try {
|
|
@@ -259,10 +259,6 @@ function loadStoredRenderTimeline(projectMarchDir, sessionId) {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
function shouldPersistRenderEvent(method) {
|
|
263
|
-
return method === "turnEnd" || method === "assistantReplyEnd" || method === "toolEnd" || method === "writeln" || method === "clearOutput";
|
|
264
|
-
}
|
|
265
|
-
|
|
266
262
|
async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, ui }) {
|
|
267
263
|
if (activation?.type !== "workspace-session" || !activation.projectId) return;
|
|
268
264
|
if (!workspaceSupervisor) throw new Error("workspace supervisor is not ready");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createToolCardBlock, writeToolEnd } from "../tool-rendering.mjs";
|
|
2
|
+
import { formatRecallLines } from "../recall-rendering.mjs";
|
|
3
|
+
|
|
4
|
+
export function restoreTimelineBlocksToOutputBuffer(output, blocks) {
|
|
5
|
+
output.clear();
|
|
6
|
+
for (const block of Array.isArray(blocks) ? blocks : []) appendTimelineBlock(output, block);
|
|
7
|
+
output.invalidate?.();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function appendTimelineBlock(output, block) {
|
|
11
|
+
if (!block || typeof block !== "object") return;
|
|
12
|
+
switch (block.type) {
|
|
13
|
+
case "assistant":
|
|
14
|
+
if (block.content) output.addBlock({ type: "markdown", text: String(block.content), sealed: Boolean(block.closed), cache: new Map() });
|
|
15
|
+
break;
|
|
16
|
+
case "thinking":
|
|
17
|
+
output.addBlock({ type: "thinking", tokens: block.tokens ?? 0, content: splitLines(block.content) });
|
|
18
|
+
break;
|
|
19
|
+
case "tool": {
|
|
20
|
+
const card = createToolCardBlock({ name: block.name, args: block.args });
|
|
21
|
+
output.addBlock(card);
|
|
22
|
+
if (block.closed) writeToolEnd({ output, name: block.name, isError: block.isError, result: block.result, toolBlock: card });
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "output":
|
|
26
|
+
if (block.newline) output.writeln(String(block.content ?? ""));
|
|
27
|
+
else output.write(String(block.content ?? ""));
|
|
28
|
+
break;
|
|
29
|
+
case "status":
|
|
30
|
+
output.addBlock({ type: "status", lines: [String(block.content ?? "")] });
|
|
31
|
+
break;
|
|
32
|
+
case "recall":
|
|
33
|
+
output.addBlock({ type: "plain", lines: formatRecallLines(block.hints ?? []) });
|
|
34
|
+
break;
|
|
35
|
+
case "editDiff":
|
|
36
|
+
output.addBlock({ type: "diff", path: block.path, diffLines: block.diffLines ?? [] });
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitLines(value) {
|
|
44
|
+
return String(value ?? "").split("\n");
|
|
45
|
+
}
|
|
@@ -2,6 +2,7 @@ import { brightBlack, dim } from "./ui-theme.mjs";
|
|
|
2
2
|
import { renderToolCardBlock } from "./output/tool-card-renderer.mjs";
|
|
3
3
|
import { renderMarkdown, renderStreamingMarkdown } from "./markdown-renderer.mjs";
|
|
4
4
|
import { renderEditDiffBlock } from "./tui-diff-rendering.mjs";
|
|
5
|
+
import { restoreTimelineBlocksToOutputBuffer } from "./output/timeline-block-restore.mjs";
|
|
5
6
|
import { OutputScrollState } from "./output/scroll-state.mjs";
|
|
6
7
|
import { appendTextLines, wrapLine } from "./output/text-line-renderer.mjs";
|
|
7
8
|
import { appendSelectableEntries, copySourceTextForRange, sliceEntriesWithTail } from "./output/selectable-copy.mjs";
|
|
@@ -89,6 +90,10 @@ export class OutputBuffer {
|
|
|
89
90
|
this._baseEntriesCache = new Map();
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
restoreTimelineBlocks(blocks) {
|
|
94
|
+
restoreTimelineBlocksToOutputBuffer(this, blocks);
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
write(text) { this._writeText(text, false); }
|
|
93
98
|
writeMarkdown(text) { this._writeText(text, true); }
|
|
94
99
|
|
|
@@ -40,6 +40,14 @@ export function createTuiInputController({ editor, requestRender, historyStore =
|
|
|
40
40
|
requestRender();
|
|
41
41
|
},
|
|
42
42
|
|
|
43
|
+
clearInput() {
|
|
44
|
+
attachmentTokens.clear();
|
|
45
|
+
editor.lastAction = null;
|
|
46
|
+
editor.historyIndex = -1;
|
|
47
|
+
setEditorText("");
|
|
48
|
+
requestRender();
|
|
49
|
+
},
|
|
50
|
+
|
|
43
51
|
insertAttachmentAtCursor({ marker, label }) {
|
|
44
52
|
const token = uniqueAttachmentToken(label || "[image]", attachmentTokens);
|
|
45
53
|
attachmentTokens.set(token, marker);
|
|
@@ -59,4 +67,12 @@ export function createTuiInputController({ editor, requestRender, historyStore =
|
|
|
59
67
|
historyStore?.save?.(editor.history);
|
|
60
68
|
} catch {}
|
|
61
69
|
}
|
|
70
|
+
|
|
71
|
+
function setEditorText(text) {
|
|
72
|
+
if (typeof editor.setTextInternal === "function") {
|
|
73
|
+
editor.setTextInternal(text);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
editor.setText?.(text);
|
|
77
|
+
}
|
|
62
78
|
}
|
package/src/cli/ui.mjs
CHANGED
|
@@ -87,6 +87,7 @@ export function createTuiUI({
|
|
|
87
87
|
thinkingSelector: () => onCtrlTHandler?.(),
|
|
88
88
|
modelSelector: () => onCtrlLHandler?.(),
|
|
89
89
|
externalEditor: () => openExternalEditor(),
|
|
90
|
+
clearInput: () => inputController.clearInput(),
|
|
90
91
|
toggleToolOutput: () => toggleToolOutput(),
|
|
91
92
|
toggleShellDrawer: () => shellDrawerControls.toggle(),
|
|
92
93
|
nextShell: () => shellDrawerControls.selectNext(),
|
|
@@ -216,10 +217,13 @@ export function createTuiUI({
|
|
|
216
217
|
},
|
|
217
218
|
|
|
218
219
|
clearOutput: () => {
|
|
219
|
-
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); requestRender();
|
|
220
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); activeToolBlocks.length = 0; output.clear(); requestRender();
|
|
221
|
+
},
|
|
222
|
+
restoreTimelineBlocks: (blocks) => {
|
|
223
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); activeToolBlocks.length = 0; output.restoreTimelineBlocks(blocks); requestRender();
|
|
220
224
|
},
|
|
221
225
|
restoreTranscript: (turns) => {
|
|
222
|
-
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); writeTranscriptToOutput(output, turns); requestRender();
|
|
226
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); activeToolBlocks.length = 0; output.clear(); writeTranscriptToOutput(output, turns); requestRender();
|
|
223
227
|
},
|
|
224
228
|
|
|
225
229
|
setStatusBar: (text) => {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createTuiTimelineRegistry } from "./tui-timeline.mjs";
|
|
2
|
+
|
|
3
|
+
const PERSIST_FLUSH_METHODS = new Set(["turnEnd", "assistantReplyEnd", "toolEnd"]);
|
|
4
|
+
|
|
1
5
|
const RENDER_METHODS = new Set([
|
|
2
6
|
"turnStart",
|
|
3
7
|
"turnEnd",
|
|
@@ -19,11 +23,18 @@ const RENDER_METHODS = new Set([
|
|
|
19
23
|
"clearOutput",
|
|
20
24
|
]);
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
export function createWorkspaceOutputRouter({
|
|
27
|
+
ui,
|
|
28
|
+
activeProjectId,
|
|
29
|
+
activeSessionId = null,
|
|
30
|
+
onPersistRenderTimeline = null,
|
|
31
|
+
persistDebounceMs,
|
|
32
|
+
} = {}) {
|
|
25
33
|
let active = routeKey(activeProjectId, activeSessionId);
|
|
26
|
-
const
|
|
34
|
+
const timelineRegistry = createTuiTimelineRegistry({
|
|
35
|
+
persistDebounceMs,
|
|
36
|
+
onPersistTimeline: (change) => onPersistRenderTimeline?.({ ...parseRouteKey(change.key), ...change }),
|
|
37
|
+
});
|
|
27
38
|
|
|
28
39
|
return {
|
|
29
40
|
setActiveProject(projectId) {
|
|
@@ -31,8 +42,9 @@ export function createWorkspaceOutputRouter({ ui, activeProjectId, activeSession
|
|
|
31
42
|
},
|
|
32
43
|
setActiveSession(projectId, sessionId, { renderTimeline = null } = {}) {
|
|
33
44
|
const next = routeKey(projectId, sessionId);
|
|
34
|
-
|
|
45
|
+
timelineRegistry.ensure(next, { events: renderTimeline });
|
|
35
46
|
if (next === active) return renderRoute(next);
|
|
47
|
+
timelineRegistry.flush(active, "session-switch");
|
|
36
48
|
active = next;
|
|
37
49
|
return renderRoute(next);
|
|
38
50
|
},
|
|
@@ -72,43 +84,43 @@ export function createWorkspaceOutputRouter({ ui, activeProjectId, activeSession
|
|
|
72
84
|
return renderRoute(active);
|
|
73
85
|
},
|
|
74
86
|
getRenderEvents(projectId, sessionId = null) {
|
|
75
|
-
return
|
|
87
|
+
return timelineRegistry.getEvents(routeKey(projectId, sessionId));
|
|
88
|
+
},
|
|
89
|
+
getRenderBlocks(projectId, sessionId = null) {
|
|
90
|
+
return timelineRegistry.getBlocks(routeKey(projectId, sessionId));
|
|
76
91
|
},
|
|
77
92
|
setRenderEvents(projectId, sessionId = null, events = []) {
|
|
78
|
-
|
|
93
|
+
const key = routeKey(projectId, sessionId);
|
|
94
|
+
timelineRegistry.replaceEvents(key, events);
|
|
79
95
|
},
|
|
80
96
|
getRenderEventCount(projectId, sessionId = null) {
|
|
81
|
-
return
|
|
97
|
+
return timelineRegistry.getEventCount(routeKey(projectId, sessionId));
|
|
98
|
+
},
|
|
99
|
+
getRenderTimelineMetadata(projectId, sessionId = null) {
|
|
100
|
+
return timelineRegistry.getMetadata(routeKey(projectId, sessionId));
|
|
101
|
+
},
|
|
102
|
+
flushRenderTimeline(projectId, sessionId = null, reason = "manual") {
|
|
103
|
+
return timelineRegistry.flush(routeKey(projectId, sessionId), reason);
|
|
104
|
+
},
|
|
105
|
+
flushAllRenderTimelines(reason = "manual") {
|
|
106
|
+
return timelineRegistry.flushAll(reason);
|
|
82
107
|
},
|
|
83
108
|
};
|
|
84
109
|
|
|
85
110
|
function renderRoute(key) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return events.length;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function applyRenderEvent({ method, args }) {
|
|
93
|
-
const value = ui[method];
|
|
94
|
-
if (typeof value === "function") value.apply(ui, args);
|
|
111
|
+
const timeline = timelineRegistry.ensure(key);
|
|
112
|
+
if (typeof ui.restoreTimelineBlocks !== "function") ui.clearOutput?.();
|
|
113
|
+
return timeline.replayTo(ui);
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
function recordRenderEvent(key, method, args) {
|
|
98
117
|
if (method === "clearOutput") {
|
|
99
|
-
|
|
100
|
-
onRenderTimelineChange?.({ ...parseRouteKey(key), events: [], event: { method, args } });
|
|
118
|
+
timelineRegistry.clear(key);
|
|
101
119
|
return;
|
|
102
120
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
timelines.set(key, events);
|
|
107
|
-
onRenderTimelineChange?.({ ...parseRouteKey(key), events: [...events], event: { method, args } });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function setRenderEvents(key, events) {
|
|
111
|
-
timelines.set(key, normalizeRenderEvents(events));
|
|
121
|
+
const timeline = timelineRegistry.ensure(key);
|
|
122
|
+
timeline.apply(method, args);
|
|
123
|
+
if (PERSIST_FLUSH_METHODS.has(method)) timeline.flushPersist(method);
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
|
|
@@ -120,8 +132,3 @@ function parseRouteKey(key) {
|
|
|
120
132
|
const [projectId, sessionId = ""] = String(key ?? "").split(":", 2);
|
|
121
133
|
return { projectId: projectId || null, sessionId: sessionId || null };
|
|
122
134
|
}
|
|
123
|
-
|
|
124
|
-
function normalizeRenderEvents(events) {
|
|
125
|
-
if (!Array.isArray(events)) return [];
|
|
126
|
-
return events.filter((event) => typeof event?.method === "string" && Array.isArray(event.args));
|
|
127
|
-
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export function createTuiTimelineProjection() {
|
|
2
|
+
let blocks = [];
|
|
3
|
+
let openAssistantBlock = null;
|
|
4
|
+
let openThinkingBlock = null;
|
|
5
|
+
let openToolBlocks = [];
|
|
6
|
+
let nextBlockIndex = 1;
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
apply(event) {
|
|
10
|
+
applyProjectionEvent(event);
|
|
11
|
+
},
|
|
12
|
+
rebuild(events) {
|
|
13
|
+
resetProjection();
|
|
14
|
+
for (const event of events) applyProjectionEvent(event);
|
|
15
|
+
},
|
|
16
|
+
clear() {
|
|
17
|
+
resetProjection();
|
|
18
|
+
},
|
|
19
|
+
getBlocks() {
|
|
20
|
+
return blocks.map((block) => structuredCloneSafe(block));
|
|
21
|
+
},
|
|
22
|
+
getMetadata() {
|
|
23
|
+
return {
|
|
24
|
+
blockCount: blocks.length,
|
|
25
|
+
openAssistant: Boolean(openAssistantBlock),
|
|
26
|
+
openThinking: Boolean(openThinkingBlock),
|
|
27
|
+
openToolCount: openToolBlocks.length,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function applyProjectionEvent(event) {
|
|
33
|
+
const [first, second, third] = event.args ?? [];
|
|
34
|
+
switch (event.method) {
|
|
35
|
+
case "turnStart":
|
|
36
|
+
closeAssistantBlock();
|
|
37
|
+
blocks.push(createBlock("turn", event.at, { phase: "start" }));
|
|
38
|
+
break;
|
|
39
|
+
case "turnEnd":
|
|
40
|
+
closeAssistantBlock(event.at);
|
|
41
|
+
closeThinkingBlock(event.at);
|
|
42
|
+
blocks.push(createBlock("turn", event.at, { phase: "end" }));
|
|
43
|
+
break;
|
|
44
|
+
case "textDelta":
|
|
45
|
+
ensureAssistantBlock(event.at).content += String(first ?? "");
|
|
46
|
+
touchBlock(openAssistantBlock, event.at);
|
|
47
|
+
break;
|
|
48
|
+
case "assistantReplyEnd":
|
|
49
|
+
closeAssistantBlock(event.at);
|
|
50
|
+
break;
|
|
51
|
+
case "thinkingStart":
|
|
52
|
+
closeThinkingBlock(event.at);
|
|
53
|
+
openThinkingBlock = createBlock("thinking", event.at, { content: "", closed: false });
|
|
54
|
+
blocks.push(openThinkingBlock);
|
|
55
|
+
break;
|
|
56
|
+
case "thinkingDelta":
|
|
57
|
+
ensureThinkingBlock(event.at).content += String(first ?? "");
|
|
58
|
+
touchBlock(openThinkingBlock, event.at);
|
|
59
|
+
break;
|
|
60
|
+
case "thinkingEnd":
|
|
61
|
+
ensureThinkingBlock(event.at).tokens = first ?? null;
|
|
62
|
+
closeThinkingBlock(event.at);
|
|
63
|
+
break;
|
|
64
|
+
case "thinkingBlock":
|
|
65
|
+
closeThinkingBlock(event.at);
|
|
66
|
+
blocks.push(createBlock("thinking", event.at, { tokens: first ?? null, content: String(second ?? ""), closed: true }));
|
|
67
|
+
break;
|
|
68
|
+
case "toolStart": {
|
|
69
|
+
closeAssistantBlock(event.at);
|
|
70
|
+
const block = createBlock("tool", event.at, { name: first ?? null, args: second ?? null, result: null, isError: false, closed: false });
|
|
71
|
+
blocks.push(block);
|
|
72
|
+
openToolBlocks.push(block);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "toolEnd": {
|
|
76
|
+
const block = popOpenToolBlock(first);
|
|
77
|
+
if (block) {
|
|
78
|
+
block.name ??= first ?? null;
|
|
79
|
+
block.isError = Boolean(second);
|
|
80
|
+
block.result = third ?? null;
|
|
81
|
+
block.closed = true;
|
|
82
|
+
touchBlock(block, event.at);
|
|
83
|
+
} else {
|
|
84
|
+
blocks.push(createBlock("tool", event.at, { name: first ?? null, isError: Boolean(second), result: third ?? null, closed: true }));
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "write":
|
|
89
|
+
case "writeln":
|
|
90
|
+
closeAssistantBlock(event.at);
|
|
91
|
+
blocks.push(createBlock("output", event.at, { content: String(first ?? ""), newline: event.method === "writeln" }));
|
|
92
|
+
break;
|
|
93
|
+
case "status":
|
|
94
|
+
blocks.push(createBlock("status", event.at, { content: String(first ?? "") }));
|
|
95
|
+
break;
|
|
96
|
+
case "recall":
|
|
97
|
+
blocks.push(createBlock("recall", event.at, { hints: first?.hints ?? [] }));
|
|
98
|
+
break;
|
|
99
|
+
case "editDiff":
|
|
100
|
+
closeAssistantBlock(event.at);
|
|
101
|
+
blocks.push(createBlock("editDiff", event.at, { path: first ?? null, diffLines: second ?? [] }));
|
|
102
|
+
break;
|
|
103
|
+
case "retryStart":
|
|
104
|
+
case "retryEnd":
|
|
105
|
+
blocks.push(createBlock("retry", event.at, { method: event.method, payload: first ?? null }));
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
blocks.push(createBlock("event", event.at, { method: event.method, args: event.args ?? [] }));
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureAssistantBlock(at) {
|
|
114
|
+
if (!openAssistantBlock) {
|
|
115
|
+
openAssistantBlock = createBlock("assistant", at, { content: "", closed: false });
|
|
116
|
+
blocks.push(openAssistantBlock);
|
|
117
|
+
}
|
|
118
|
+
return openAssistantBlock;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function closeAssistantBlock(at = null) {
|
|
122
|
+
if (!openAssistantBlock) return;
|
|
123
|
+
openAssistantBlock.closed = true;
|
|
124
|
+
touchBlock(openAssistantBlock, at);
|
|
125
|
+
openAssistantBlock = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ensureThinkingBlock(at) {
|
|
129
|
+
if (!openThinkingBlock) {
|
|
130
|
+
openThinkingBlock = createBlock("thinking", at, { content: "", closed: false });
|
|
131
|
+
blocks.push(openThinkingBlock);
|
|
132
|
+
}
|
|
133
|
+
return openThinkingBlock;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function closeThinkingBlock(at = null) {
|
|
137
|
+
if (!openThinkingBlock) return;
|
|
138
|
+
openThinkingBlock.closed = true;
|
|
139
|
+
touchBlock(openThinkingBlock, at);
|
|
140
|
+
openThinkingBlock = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function popOpenToolBlock(name) {
|
|
144
|
+
if (openToolBlocks.length === 0) return null;
|
|
145
|
+
if (name == null) return openToolBlocks.pop();
|
|
146
|
+
const index = findLastIndex(openToolBlocks, (block) => block.name === name);
|
|
147
|
+
if (index < 0) return openToolBlocks.pop();
|
|
148
|
+
return openToolBlocks.splice(index, 1)[0];
|
|
149
|
+
}
|
|
150
|
+
function resetProjection() {
|
|
151
|
+
blocks = [];
|
|
152
|
+
openAssistantBlock = null;
|
|
153
|
+
openThinkingBlock = null;
|
|
154
|
+
openToolBlocks = [];
|
|
155
|
+
nextBlockIndex = 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createBlock(type, at, fields = {}) {
|
|
159
|
+
const id = `${type}-${nextBlockIndex++}`;
|
|
160
|
+
return { id, type, createdAt: at ?? null, updatedAt: at ?? null, ...fields };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function touchBlock(block, at = null) {
|
|
165
|
+
if (!block || at == null) return;
|
|
166
|
+
block.updatedAt = at;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function findLastIndex(items, predicate) {
|
|
170
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
171
|
+
if (predicate(items[index], index)) return index;
|
|
172
|
+
}
|
|
173
|
+
return -1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function structuredCloneSafe(value) {
|
|
177
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
178
|
+
return JSON.parse(JSON.stringify(value));
|
|
179
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createTuiTimelineProjection } from "./tui-timeline-projection.mjs";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MAX_TIMELINE_EVENTS = 4000;
|
|
4
|
+
export const DEFAULT_TIMELINE_PERSIST_DEBOUNCE_MS = 250;
|
|
5
|
+
|
|
6
|
+
export function createTuiTimelineRegistry({
|
|
7
|
+
maxEventsPerTimeline = DEFAULT_MAX_TIMELINE_EVENTS,
|
|
8
|
+
persistDebounceMs = DEFAULT_TIMELINE_PERSIST_DEBOUNCE_MS,
|
|
9
|
+
onPersistTimeline = null,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const timelines = new Map();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
ensure(key, { events = null } = {}) {
|
|
15
|
+
let timeline = timelines.get(key);
|
|
16
|
+
if (!timeline) {
|
|
17
|
+
timeline = createTuiTimelineInstance({
|
|
18
|
+
key,
|
|
19
|
+
maxEvents: maxEventsPerTimeline,
|
|
20
|
+
persistDebounceMs,
|
|
21
|
+
onPersist: onPersistTimeline,
|
|
22
|
+
});
|
|
23
|
+
timelines.set(key, timeline);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(events)) timeline.hydrateIfEmpty(events);
|
|
26
|
+
return timeline;
|
|
27
|
+
},
|
|
28
|
+
get(key) {
|
|
29
|
+
return timelines.get(key) ?? null;
|
|
30
|
+
},
|
|
31
|
+
has(key) {
|
|
32
|
+
return timelines.has(key);
|
|
33
|
+
},
|
|
34
|
+
clear(key, options) {
|
|
35
|
+
const timeline = this.ensure(key);
|
|
36
|
+
timeline.clear(options);
|
|
37
|
+
return timeline;
|
|
38
|
+
},
|
|
39
|
+
flush(key, reason = "manual") {
|
|
40
|
+
return this.get(key)?.flushPersist(reason) ?? false;
|
|
41
|
+
},
|
|
42
|
+
flushAll(reason = "manual") {
|
|
43
|
+
let flushed = 0;
|
|
44
|
+
for (const timeline of timelines.values()) {
|
|
45
|
+
if (timeline.flushPersist(reason)) flushed += 1;
|
|
46
|
+
}
|
|
47
|
+
return flushed;
|
|
48
|
+
},
|
|
49
|
+
replaceEvents(key, events) {
|
|
50
|
+
const timeline = this.ensure(key);
|
|
51
|
+
timeline.replaceEvents(events);
|
|
52
|
+
return timeline;
|
|
53
|
+
},
|
|
54
|
+
getEvents(key) {
|
|
55
|
+
return this.get(key)?.getEvents() ?? [];
|
|
56
|
+
},
|
|
57
|
+
getBlocks(key) {
|
|
58
|
+
return this.get(key)?.getBlocks() ?? [];
|
|
59
|
+
},
|
|
60
|
+
getEventCount(key) {
|
|
61
|
+
return this.get(key)?.getEventCount() ?? 0;
|
|
62
|
+
},
|
|
63
|
+
getMetadata(key) {
|
|
64
|
+
return this.get(key)?.getMetadata() ?? null;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createTuiTimelineInstance({
|
|
70
|
+
key,
|
|
71
|
+
maxEvents = DEFAULT_MAX_TIMELINE_EVENTS,
|
|
72
|
+
persistDebounceMs = DEFAULT_TIMELINE_PERSIST_DEBOUNCE_MS,
|
|
73
|
+
onPersist = null,
|
|
74
|
+
} = {}) {
|
|
75
|
+
let events = [];
|
|
76
|
+
const projection = createTuiTimelineProjection();
|
|
77
|
+
let hydrated = false;
|
|
78
|
+
let dirty = false;
|
|
79
|
+
let lastAccessedAt = Date.now();
|
|
80
|
+
let lastUpdatedAt = null;
|
|
81
|
+
let lastPersistedAt = null;
|
|
82
|
+
let estimatedBytes = 0;
|
|
83
|
+
let persistTimer = null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
key,
|
|
87
|
+
apply(method, args, { at = Date.now(), persist = true } = {}) {
|
|
88
|
+
touch();
|
|
89
|
+
const event = { method, args, at };
|
|
90
|
+
events.push(event);
|
|
91
|
+
projection.apply(event);
|
|
92
|
+
trimToBudget();
|
|
93
|
+
dirty = true;
|
|
94
|
+
lastUpdatedAt = at;
|
|
95
|
+
if (persist) schedulePersist("debounce");
|
|
96
|
+
return event;
|
|
97
|
+
},
|
|
98
|
+
hydrateIfEmpty(nextEvents) {
|
|
99
|
+
touch();
|
|
100
|
+
if (events.length > 0) return false;
|
|
101
|
+
replaceEvents(nextEvents, { markHydrated: true });
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
replaceEvents(nextEvents) {
|
|
105
|
+
touch();
|
|
106
|
+
replaceEvents(nextEvents, { markHydrated: false });
|
|
107
|
+
},
|
|
108
|
+
clear({ flush = true } = {}) {
|
|
109
|
+
touch();
|
|
110
|
+
events = [];
|
|
111
|
+
projection.clear();
|
|
112
|
+
dirty = true;
|
|
113
|
+
lastUpdatedAt = Date.now();
|
|
114
|
+
estimatedBytes = 0;
|
|
115
|
+
if (flush) this.flushPersist("clear");
|
|
116
|
+
else schedulePersist("debounce");
|
|
117
|
+
},
|
|
118
|
+
replayTo(ui) {
|
|
119
|
+
touch();
|
|
120
|
+
if (typeof ui.restoreTimelineBlocks === "function") {
|
|
121
|
+
const blocks = projection.getBlocks();
|
|
122
|
+
ui.restoreTimelineBlocks(blocks);
|
|
123
|
+
return blocks.length;
|
|
124
|
+
}
|
|
125
|
+
for (const event of events) applyRenderEvent(ui, event);
|
|
126
|
+
return events.length;
|
|
127
|
+
},
|
|
128
|
+
flushPersist(reason = "manual") {
|
|
129
|
+
clearPersistTimer();
|
|
130
|
+
if (!dirty || typeof onPersist !== "function") return false;
|
|
131
|
+
onPersist({ key, events: this.getEvents(), reason, timeline: this.getMetadata() });
|
|
132
|
+
dirty = false;
|
|
133
|
+
lastPersistedAt = Date.now();
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
getEvents() {
|
|
137
|
+
touch();
|
|
138
|
+
return cloneEvents(events);
|
|
139
|
+
},
|
|
140
|
+
getBlocks() {
|
|
141
|
+
touch();
|
|
142
|
+
return projection.getBlocks();
|
|
143
|
+
},
|
|
144
|
+
getEventCount() {
|
|
145
|
+
return events.length;
|
|
146
|
+
},
|
|
147
|
+
markPersisted() {
|
|
148
|
+
clearPersistTimer();
|
|
149
|
+
dirty = false;
|
|
150
|
+
lastPersistedAt = Date.now();
|
|
151
|
+
},
|
|
152
|
+
getMetadata() {
|
|
153
|
+
return buildMetadata();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function schedulePersist(reason) {
|
|
158
|
+
if (typeof onPersist !== "function") return;
|
|
159
|
+
clearPersistTimer();
|
|
160
|
+
persistTimer = setTimeout(() => {
|
|
161
|
+
persistTimer = null;
|
|
162
|
+
if (!dirty) return;
|
|
163
|
+
onPersist({ key, events: cloneEvents(events), reason, timeline: buildMetadata() });
|
|
164
|
+
dirty = false;
|
|
165
|
+
lastPersistedAt = Date.now();
|
|
166
|
+
}, Math.max(0, persistDebounceMs));
|
|
167
|
+
persistTimer.unref?.();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function clearPersistTimer() {
|
|
171
|
+
if (!persistTimer) return;
|
|
172
|
+
clearTimeout(persistTimer);
|
|
173
|
+
persistTimer = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function replaceEvents(nextEvents, { markHydrated }) {
|
|
177
|
+
clearPersistTimer();
|
|
178
|
+
events = normalizeTimelineEvents(nextEvents);
|
|
179
|
+
trimToBudget();
|
|
180
|
+
rebuildProjection();
|
|
181
|
+
hydrated = markHydrated;
|
|
182
|
+
dirty = false;
|
|
183
|
+
lastUpdatedAt = events.at(-1)?.at ?? null;
|
|
184
|
+
lastPersistedAt = null;
|
|
185
|
+
updateEstimatedBytes();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function trimToBudget() {
|
|
189
|
+
if (events.length > maxEvents) {
|
|
190
|
+
events.splice(0, events.length - maxEvents);
|
|
191
|
+
rebuildProjection();
|
|
192
|
+
}
|
|
193
|
+
updateEstimatedBytes();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function rebuildProjection() {
|
|
197
|
+
projection.rebuild(events);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function updateEstimatedBytes() {
|
|
201
|
+
estimatedBytes = estimateJsonBytes(events) + estimateJsonBytes(projection.getBlocks());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function touch() {
|
|
205
|
+
lastAccessedAt = Date.now();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildMetadata() {
|
|
209
|
+
return {
|
|
210
|
+
key,
|
|
211
|
+
eventCount: events.length,
|
|
212
|
+
maxEvents,
|
|
213
|
+
hydrated,
|
|
214
|
+
dirty,
|
|
215
|
+
lastAccessedAt,
|
|
216
|
+
lastUpdatedAt,
|
|
217
|
+
lastPersistedAt,
|
|
218
|
+
estimatedBytes,
|
|
219
|
+
persistScheduled: Boolean(persistTimer),
|
|
220
|
+
...projection.getMetadata(),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function normalizeTimelineEvents(events) {
|
|
226
|
+
if (!Array.isArray(events)) return [];
|
|
227
|
+
return events
|
|
228
|
+
.filter((event) => typeof event?.method === "string" && Array.isArray(event.args))
|
|
229
|
+
.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function applyRenderEvent(ui, { method, args }) {
|
|
233
|
+
const value = ui[method];
|
|
234
|
+
if (typeof value === "function") value.apply(ui, args);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cloneEvents(items) {
|
|
238
|
+
return items.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function estimateJsonBytes(value) {
|
|
242
|
+
try {
|
|
243
|
+
return JSON.stringify(value).length;
|
|
244
|
+
} catch {
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -45,7 +45,7 @@ export function saveMarchSessionState({ projectMarchDir, sessionId, engine, back
|
|
|
45
45
|
export function saveMarchSessionStateValue({ projectMarchDir, sessionId, state }) {
|
|
46
46
|
if (!sessionId) throw new Error("March session id is required");
|
|
47
47
|
const existing = loadMarchSessionState({ projectMarchDir, sessionId })?.state ?? null;
|
|
48
|
-
const nextState = normalizeMarchSessionStateForSave({ ...existing, ...state
|
|
48
|
+
const nextState = normalizeMarchSessionStateForSave({ ...existing, ...state });
|
|
49
49
|
validateMarchSessionState(nextState);
|
|
50
50
|
const dir = getMarchSessionStateDir(projectMarchDir, sessionId);
|
|
51
51
|
mkdirSync(dir, { recursive: true });
|
|
@@ -135,22 +135,12 @@ function validateMarchSessionState(state) {
|
|
|
135
135
|
function isValidMarchSessionState(state) {
|
|
136
136
|
return state?.version === MARCH_SESSION_STATE_VERSION
|
|
137
137
|
&& Boolean(state.cwd)
|
|
138
|
-
&& Array.isArray(state.turns)
|
|
139
|
-
&& Array.isArray(state.renderTimeline);
|
|
138
|
+
&& Array.isArray(state.turns);
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
function normalizeMarchSessionStateForSave(state) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
renderTimeline: normalizePersistedRenderTimeline(state.renderTimeline),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function normalizePersistedRenderTimeline(events) {
|
|
150
|
-
if (!Array.isArray(events)) return [];
|
|
151
|
-
return events
|
|
152
|
-
.filter((event) => typeof event?.method === "string" && Array.isArray(event.args))
|
|
153
|
-
.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
142
|
+
const { renderTimeline: _renderTimeline, renderTimelineUpdatedAt: _renderTimelineUpdatedAt, ...coreState } = state ?? {};
|
|
143
|
+
return coreState;
|
|
154
144
|
}
|
|
155
145
|
|
|
156
146
|
function normalizeSessionRef(sessionRef) {
|
|
@@ -1,29 +1,45 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getMarchSessionStatePath, loadMarchSessionState, normalizeSessionId } from "./march-session-state.mjs";
|
|
4
|
+
|
|
5
|
+
export const TUI_SESSION_TIMELINE_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export function getTuiSessionUiStateRoot(projectMarchDir) {
|
|
8
|
+
return join(projectMarchDir, "ui", "tui", "sessions");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getTuiSessionTimelinePath(projectMarchDir, sessionId) {
|
|
12
|
+
return join(getTuiSessionUiStateRoot(projectMarchDir), normalizeSessionId(sessionId), "timeline.json");
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
export function loadMarchSessionRenderTimeline({ projectMarchDir, sessionId }) {
|
|
5
|
-
const
|
|
16
|
+
const tuiTimeline = loadTuiRenderTimeline({ projectMarchDir, sessionId });
|
|
17
|
+
if (tuiTimeline) return tuiTimeline;
|
|
18
|
+
|
|
19
|
+
const legacyTimeline = loadLegacyCoreRenderTimeline({ projectMarchDir, sessionId });
|
|
20
|
+
if (legacyTimeline) return legacyTimeline;
|
|
21
|
+
|
|
6
22
|
const stored = loadMarchSessionState({ projectMarchDir, sessionId });
|
|
7
23
|
if (!stored) return null;
|
|
8
|
-
const renderTimeline = normalizeSessionRenderTimeline(stored.state.renderTimeline);
|
|
9
24
|
return {
|
|
10
25
|
path: stored.path,
|
|
11
|
-
renderTimeline:
|
|
26
|
+
renderTimeline: renderTimelineFromTurns(stored.state.turns ?? []),
|
|
27
|
+
source: "core-turns",
|
|
12
28
|
};
|
|
13
29
|
}
|
|
14
30
|
|
|
15
31
|
export function saveMarchSessionRenderTimeline({ projectMarchDir, sessionId, renderTimeline }) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
if (!sessionId) throw new Error("March session id is required");
|
|
33
|
+
const state = {
|
|
34
|
+
version: TUI_SESSION_TIMELINE_VERSION,
|
|
35
|
+
savedAt: new Date().toISOString(),
|
|
20
36
|
sessionId,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
37
|
+
renderTimeline: normalizeSessionRenderTimeline(renderTimeline),
|
|
38
|
+
};
|
|
39
|
+
const path = getTuiSessionTimelinePath(projectMarchDir, sessionId);
|
|
40
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
41
|
+
writeFileSync(path, JSON.stringify(state, null, 2), "utf8");
|
|
42
|
+
return { path, renderTimeline: state.renderTimeline, state };
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
export function normalizeSessionRenderTimeline(events) {
|
|
@@ -33,15 +49,28 @@ export function normalizeSessionRenderTimeline(events) {
|
|
|
33
49
|
.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
function
|
|
52
|
+
function loadTuiRenderTimeline({ projectMarchDir, sessionId }) {
|
|
53
|
+
try {
|
|
54
|
+
const path = getTuiSessionTimelinePath(projectMarchDir, sessionId);
|
|
55
|
+
if (!existsSync(path)) return null;
|
|
56
|
+
const state = JSON.parse(readFileSync(path, "utf8"));
|
|
57
|
+
if (state?.version !== TUI_SESSION_TIMELINE_VERSION) return null;
|
|
58
|
+
return { path, renderTimeline: normalizeSessionRenderTimeline(state.renderTimeline), source: "tui" };
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadLegacyCoreRenderTimeline({ projectMarchDir, sessionId }) {
|
|
37
65
|
try {
|
|
38
66
|
const path = getMarchSessionStatePath(projectMarchDir, sessionId);
|
|
39
|
-
if (!existsSync(path)) return
|
|
67
|
+
if (!existsSync(path)) return null;
|
|
40
68
|
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
41
69
|
const rawTimeline = normalizeSessionRenderTimeline(raw.renderTimeline);
|
|
42
|
-
|
|
70
|
+
if (!raw.renderTimelineUpdatedAt && rawTimeline.length === 0) return null;
|
|
71
|
+
return { path, renderTimeline: rawTimeline, source: "legacy-core-render" };
|
|
43
72
|
} catch {
|
|
44
|
-
return
|
|
73
|
+
return null;
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
76
|
|
|
@@ -96,12 +96,13 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
96
96
|
let runtime = session?.id ? runtimes.get(runtimeKey(project.projectId, session.id)) : findIdleRuntime(project.projectId);
|
|
97
97
|
if (!runtime) runtime = await createProjectRuntime(project);
|
|
98
98
|
|
|
99
|
+
const targetSessionId = session?.id ?? null;
|
|
99
100
|
let restoreState = null;
|
|
100
|
-
if (session?.path && getRuntimeSessionId(runtime) !==
|
|
101
|
+
if (session?.path && getRuntimeSessionId(runtime) !== targetSessionId) {
|
|
101
102
|
restoreState = loadWorkspaceMarchSessionState({ runtime, session });
|
|
102
103
|
await runtime.runner.switchPiSession(session.path, restoreState);
|
|
103
|
-
syncSessionState(runtime, session.id);
|
|
104
104
|
}
|
|
105
|
+
if (targetSessionId) syncSessionState(runtime, targetSessionId);
|
|
105
106
|
|
|
106
107
|
active = runtime;
|
|
107
108
|
rememberRuntime(runtime);
|
|
@@ -153,12 +154,11 @@ function loadWorkspaceMarchSessionState({ runtime, session }) {
|
|
|
153
154
|
if (stored.state.cwd && stored.state.cwd !== runtime.runner.engine.cwd) {
|
|
154
155
|
throw new Error(`March session state cwd mismatch for ${session.id}: ${stored.state.cwd}`);
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
return contextState;
|
|
157
|
+
return stored.state;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
function getRuntimeSessionId(runtime) {
|
|
161
|
-
return runtime.
|
|
161
|
+
return runtime.sessionState?.sessionId ?? runtime.runner.getSessionStats?.()?.sessionId ?? null;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function syncSessionState(runtime, sessionId) {
|