march-cli 0.1.39 → 0.1.41
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/args.mjs +1 -0
- package/src/cli/input/keybindings.mjs +2 -0
- package/src/cli/repl-loop.mjs +4 -0
- package/src/cli/startup/app-runtime.mjs +7 -8
- 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/command.mjs +44 -0
- 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/config/config-json.mjs +20 -0
- package/src/provider/command.mjs +5 -1
- package/src/provider/remove-command.mjs +129 -0
- package/src/session/control/controller-lease.mjs +149 -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/session-restore.mjs +22 -0
- package/src/workspace/supervisor.mjs +150 -36
- package/src/workspace/view-runtime.mjs +40 -0
|
@@ -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
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { loadMarchSessionStateForPiBackend } from "../session/state/march-session-state.mjs";
|
|
2
|
+
|
|
3
|
+
export function loadOptionalWorkspaceMarchSessionState({ runtime, session }) {
|
|
4
|
+
try {
|
|
5
|
+
return loadWorkspaceMarchSessionState({ runtime, session });
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadWorkspaceMarchSessionState({ runtime, session }) {
|
|
12
|
+
const stored = loadMarchSessionStateForPiBackend({
|
|
13
|
+
projectMarchDir: runtime.projectMarchDir,
|
|
14
|
+
sessionId: session.id,
|
|
15
|
+
sessionRef: session.path,
|
|
16
|
+
});
|
|
17
|
+
if (!stored) throw new Error(`March session state not found for ${session.id}; refusing partial resume`);
|
|
18
|
+
if (stored.state.cwd && stored.state.cwd !== runtime.runner.engine.cwd) {
|
|
19
|
+
throw new Error(`March session state cwd mismatch for ${session.id}: ${stored.state.cwd}`);
|
|
20
|
+
}
|
|
21
|
+
return stored.state;
|
|
22
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import {
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
|
+
import { createSessionControllerLeaseManager } from "../session/control/controller-lease.mjs";
|
|
3
|
+
import { loadOptionalWorkspaceMarchSessionState, loadWorkspaceMarchSessionState } from "./session-restore.mjs";
|
|
4
|
+
import { createViewOnlyRuntime } from "./view-runtime.mjs";
|
|
3
5
|
|
|
4
|
-
export function createWorkspaceSessionSupervisor({ initialRuntime, createProjectRuntime, viewSessionState = initialRuntime?.sessionState, onActivate = null }) {
|
|
6
|
+
export function createWorkspaceSessionSupervisor({ initialRuntime, createProjectRuntime, viewSessionState = initialRuntime?.sessionState, onActivate = null, controllerLeases = createSessionControllerLeaseManager({ cwd: initialRuntime?.cwd }) }) {
|
|
5
7
|
if (!initialRuntime?.project?.projectId) throw new Error("initial workspace runtime is missing project metadata");
|
|
6
8
|
if (typeof createProjectRuntime !== "function") throw new Error("createProjectRuntime is required");
|
|
7
9
|
|
|
8
10
|
const runtimes = new Map();
|
|
9
11
|
let active = initialRuntime;
|
|
12
|
+
let activeView = null;
|
|
10
13
|
let disposed = false;
|
|
11
14
|
rememberRuntime(initialRuntime);
|
|
12
15
|
|
|
@@ -18,15 +21,21 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
18
21
|
if (prop === "activateWorkspaceSessionById") return activateWorkspaceSessionById;
|
|
19
22
|
if (prop === "startNewWorkspaceSession") return startNewWorkspaceSession;
|
|
20
23
|
if (prop === "refreshActiveRuntime") return refreshActiveRuntime;
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (prop === "viewWorkspaceSession") return viewWorkspaceSession;
|
|
25
|
+
if (prop === "runTurn") return runActiveTurn;
|
|
26
|
+
if (prop === "abort") return abortActiveTurn;
|
|
27
|
+
if (prop === "startNewSession") return startActiveNewSession;
|
|
28
|
+
if (prop === "switchPiSession") return switchActivePiSession;
|
|
29
|
+
const current = getActive();
|
|
30
|
+
const value = current.runner[prop];
|
|
31
|
+
return typeof value === "function" ? value.bind(current.runner) : value;
|
|
23
32
|
},
|
|
24
33
|
set(_target, prop, value) {
|
|
25
|
-
|
|
34
|
+
getActive().runner[prop] = value;
|
|
26
35
|
return true;
|
|
27
36
|
},
|
|
28
37
|
has(_target, prop) {
|
|
29
|
-
return prop === "dispose" || prop === "getActiveWorkspaceRuntime" || prop === "activateWorkspaceSession" || prop === "activateWorkspaceSessionById" || prop === "startNewWorkspaceSession" || prop === "refreshActiveRuntime" || prop in
|
|
38
|
+
return prop === "dispose" || prop === "getActiveWorkspaceRuntime" || prop === "activateWorkspaceSession" || prop === "activateWorkspaceSessionById" || prop === "startNewWorkspaceSession" || prop === "refreshActiveRuntime" || prop === "viewWorkspaceSession" || prop === "runTurn" || prop === "abort" || prop === "startNewSession" || prop === "switchPiSession" || prop in getActive().runner;
|
|
30
39
|
},
|
|
31
40
|
});
|
|
32
41
|
|
|
@@ -37,6 +46,7 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
37
46
|
getRunningTurns,
|
|
38
47
|
getRuntimeSummaries,
|
|
39
48
|
refreshActiveRuntime,
|
|
49
|
+
viewWorkspaceSession,
|
|
40
50
|
activateWorkspaceSession,
|
|
41
51
|
activateWorkspaceSessionById,
|
|
42
52
|
startNewWorkspaceSession,
|
|
@@ -44,7 +54,7 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
44
54
|
};
|
|
45
55
|
|
|
46
56
|
function getActive() {
|
|
47
|
-
return active;
|
|
57
|
+
return activeView ?? active;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
function hasRunningTurn() {
|
|
@@ -65,9 +75,10 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function refreshActiveRuntime() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
const current = getActive();
|
|
79
|
+
if (!current.viewOnly) rememberRuntime(current);
|
|
80
|
+
mirrorSessionState(viewSessionState, current.sessionState);
|
|
81
|
+
onActivate?.({ projectId: current.project.projectId, sessionId: getRuntimeSessionId(current), runtime: current });
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
async function activateWorkspaceSessionById({ projects = [], projectId, sessionId }) {
|
|
@@ -79,35 +90,64 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
async function startNewWorkspaceSession(project) {
|
|
93
|
+
activeView = null;
|
|
94
|
+
const previous = active;
|
|
82
95
|
const runtime = await getIdleRuntimeForProject(project);
|
|
83
96
|
active = runtime;
|
|
84
97
|
const result = await active.runner.startNewSession();
|
|
85
|
-
if (!result?.cancelled && result?.sessionId)
|
|
98
|
+
if (!result?.cancelled && result?.sessionId) {
|
|
99
|
+
syncSessionState(active, result.sessionId);
|
|
100
|
+
replaceRuntimeLease(active, acquireRuntimeLease(active, { sessionId: result.sessionId, sessionPath: result.sessionFile ?? null }));
|
|
101
|
+
releaseIdleRuntimeLease(previous, active);
|
|
102
|
+
}
|
|
86
103
|
rememberRuntime(active);
|
|
87
104
|
mirrorSessionState(viewSessionState, active.sessionState);
|
|
88
105
|
onActivate?.({ projectId: active.project.projectId, sessionId: getRuntimeSessionId(active), runtime: active, restoreState: null });
|
|
89
106
|
return { runtime: active, result };
|
|
90
107
|
}
|
|
91
108
|
|
|
92
|
-
async function activateWorkspaceSession({ project, session = null }) {
|
|
109
|
+
async function activateWorkspaceSession({ project, session = null, force = false }) {
|
|
93
110
|
if (disposed) throw new Error("workspace supervisor is already disposed");
|
|
94
111
|
if (!project?.projectId) throw new Error("workspace project is required");
|
|
95
112
|
|
|
96
113
|
let runtime = session?.id ? runtimes.get(runtimeKey(project.projectId, session.id)) : findIdleRuntime(project.projectId);
|
|
97
114
|
if (!runtime) runtime = await createProjectRuntime(project);
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
const targetSessionId = session?.id ?? null;
|
|
117
|
+
const lease = targetSessionId ? acquireRuntimeLease(runtime, { sessionId: targetSessionId, sessionPath: session?.path ?? null }, { force }) : null;
|
|
118
|
+
const previous = active;
|
|
119
|
+
try {
|
|
120
|
+
let restoreState = null;
|
|
121
|
+
if (session?.path && getRuntimeSessionId(runtime) !== targetSessionId) {
|
|
122
|
+
restoreState = loadWorkspaceMarchSessionState({ runtime, session });
|
|
123
|
+
await runtime.runner.switchPiSession(session.path, restoreState);
|
|
124
|
+
}
|
|
125
|
+
if (targetSessionId) syncSessionState(runtime, targetSessionId);
|
|
126
|
+
|
|
127
|
+
replaceRuntimeLease(runtime, lease);
|
|
128
|
+
releaseIdleRuntimeLease(previous, runtime);
|
|
129
|
+
activeView = null;
|
|
130
|
+
active = runtime;
|
|
131
|
+
rememberRuntime(runtime);
|
|
132
|
+
mirrorSessionState(viewSessionState, runtime.sessionState);
|
|
133
|
+
onActivate?.({ projectId: runtime.project.projectId, sessionId: getRuntimeSessionId(runtime), runtime, restoreState });
|
|
134
|
+
return active;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
lease?.release?.();
|
|
137
|
+
throw err;
|
|
104
138
|
}
|
|
139
|
+
}
|
|
105
140
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
function viewWorkspaceSession({ project, session }) {
|
|
142
|
+
if (disposed) throw new Error("workspace supervisor is already disposed");
|
|
143
|
+
if (!project?.projectId || !session?.id) throw new Error("workspace session is required");
|
|
144
|
+
const baseRuntime = findIdleRuntime(project.projectId, { allowSessionRuntime: false }) ?? active;
|
|
145
|
+
const restoreState = session.path ? loadOptionalWorkspaceMarchSessionState({ runtime: { ...baseRuntime, project, projectMarchDir: join(project.rootPath, ".march") }, session }) : null;
|
|
146
|
+
const view = createViewOnlyRuntime({ project, session, baseRuntime, restoreState });
|
|
147
|
+
activeView = view;
|
|
148
|
+
mirrorSessionState(viewSessionState, view.sessionState);
|
|
149
|
+
onActivate?.({ projectId: project.projectId, sessionId: session.id, runtime: view, restoreState, viewOnly: true });
|
|
150
|
+
return view;
|
|
111
151
|
}
|
|
112
152
|
|
|
113
153
|
async function getIdleRuntimeForProject(project) {
|
|
@@ -137,28 +177,98 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
137
177
|
disposed = true;
|
|
138
178
|
const uniqueRuntimes = new Set(runtimes.values());
|
|
139
179
|
await Promise.all(Array.from(uniqueRuntimes, async (runtime) => {
|
|
180
|
+
releaseRuntimeLease(runtime);
|
|
140
181
|
await runtime.runner.dispose?.();
|
|
141
182
|
runtime.memoryStore?.close?.();
|
|
142
183
|
}));
|
|
143
184
|
}
|
|
144
|
-
}
|
|
145
185
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
186
|
+
async function runActiveTurn(...args) {
|
|
187
|
+
if (activeView) throw new Error("This session is view-only. Use /session and Take over control before sending prompts.");
|
|
188
|
+
ensureRuntimeLease(active);
|
|
189
|
+
return await active.runner.runTurn(...args);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function abortActiveTurn(...args) {
|
|
193
|
+
if (activeView) throw new Error("This session is view-only; there is no local turn to abort.");
|
|
194
|
+
ensureRuntimeLease(active);
|
|
195
|
+
return active.runner.abort(...args);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function startActiveNewSession(...args) {
|
|
199
|
+
activeView = null;
|
|
200
|
+
const result = await active.runner.startNewSession(...args);
|
|
201
|
+
if (!result?.cancelled && result?.sessionId) {
|
|
202
|
+
syncSessionState(active, result.sessionId);
|
|
203
|
+
replaceRuntimeLease(active, acquireRuntimeLease(active, { sessionId: result.sessionId, sessionPath: result.sessionFile ?? null }));
|
|
204
|
+
rememberRuntime(active);
|
|
205
|
+
mirrorSessionState(viewSessionState, active.sessionState);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function switchActivePiSession(sessionPath, restoreState = null, ...args) {
|
|
211
|
+
activeView = null;
|
|
212
|
+
const sessionId = sessionIdFromSessionPath(sessionPath);
|
|
213
|
+
const lease = acquireRuntimeLease(active, { sessionId, sessionPath });
|
|
214
|
+
try {
|
|
215
|
+
const result = await active.runner.switchPiSession(sessionPath, restoreState, ...args);
|
|
216
|
+
if (result?.cancelled) {
|
|
217
|
+
lease.release?.();
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
const stats = active.runner.getSessionStats?.() ?? {};
|
|
221
|
+
syncSessionState(active, stats.sessionId ?? sessionId);
|
|
222
|
+
replaceRuntimeLease(active, lease);
|
|
223
|
+
rememberRuntime(active);
|
|
224
|
+
mirrorSessionState(viewSessionState, active.sessionState);
|
|
225
|
+
return result;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
lease.release?.();
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function acquireRuntimeLease(runtime, session, options = {}) {
|
|
233
|
+
if (!session?.sessionId) return null;
|
|
234
|
+
return controllerLeases.acquire({
|
|
235
|
+
sessionId: session.sessionId,
|
|
236
|
+
sessionPath: session.sessionPath ?? getRuntimeSessionFile(runtime),
|
|
237
|
+
projectMarchDir: runtime.projectMarchDir,
|
|
238
|
+
}, options);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ensureRuntimeLease(runtime) {
|
|
242
|
+
const sessionId = getRuntimeSessionId(runtime);
|
|
243
|
+
if (!sessionId) return null;
|
|
244
|
+
if (!runtime.controllerLease) replaceRuntimeLease(runtime, acquireRuntimeLease(runtime, { sessionId }));
|
|
245
|
+
runtime.controllerLease.assertOwned();
|
|
246
|
+
return runtime.controllerLease;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function replaceRuntimeLease(runtime, lease) {
|
|
250
|
+
if (runtime.controllerLease === lease) return;
|
|
251
|
+
runtime.controllerLease?.release?.();
|
|
252
|
+
runtime.controllerLease = lease;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function releaseRuntimeLease(runtime) {
|
|
256
|
+
runtime.controllerLease?.release?.();
|
|
257
|
+
runtime.controllerLease = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function releaseIdleRuntimeLease(runtime, nextRuntime) {
|
|
261
|
+
if (!runtime || runtime === nextRuntime || runtime.turnTask) return;
|
|
262
|
+
releaseRuntimeLease(runtime);
|
|
155
263
|
}
|
|
156
|
-
const { renderTimeline: _renderTimeline, ...contextState } = stored.state;
|
|
157
|
-
return contextState;
|
|
158
264
|
}
|
|
159
265
|
|
|
160
266
|
function getRuntimeSessionId(runtime) {
|
|
161
|
-
return runtime.
|
|
267
|
+
return runtime.sessionState?.sessionId ?? runtime.runner.getSessionStats?.()?.sessionId ?? null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getRuntimeSessionFile(runtime) {
|
|
271
|
+
return runtime.runner.getSessionStats?.()?.sessionFile ?? null;
|
|
162
272
|
}
|
|
163
273
|
|
|
164
274
|
function syncSessionState(runtime, sessionId) {
|
|
@@ -176,3 +286,7 @@ function mirrorSessionState(target, source) {
|
|
|
176
286
|
function runtimeKey(projectId, sessionId = null) {
|
|
177
287
|
return `${projectId}:${sessionId ?? ""}`;
|
|
178
288
|
}
|
|
289
|
+
|
|
290
|
+
function sessionIdFromSessionPath(sessionPath) {
|
|
291
|
+
return basename(String(sessionPath)).replace(/\.jsonl?$/i, "");
|
|
292
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function createViewOnlyRuntime({ project, session, baseRuntime, restoreState }) {
|
|
4
|
+
const projectRoot = resolveProjectRoot(project, baseRuntime);
|
|
5
|
+
const sessionState = {
|
|
6
|
+
sessionId: session.id,
|
|
7
|
+
sessionDir: join(projectRoot, ".march", "sessions", session.id),
|
|
8
|
+
};
|
|
9
|
+
const runner = {
|
|
10
|
+
engine: { cwd: projectRoot, turns: restoreState?.turns ?? [] },
|
|
11
|
+
runtimeState: { engine: { cwd: projectRoot } },
|
|
12
|
+
getSessionStats: () => ({ sessionId: session.id, sessionFile: session.path ?? null }),
|
|
13
|
+
estimateContextTokens: () => null,
|
|
14
|
+
canSwitchPiSession: () => false,
|
|
15
|
+
runTurn: rejectViewOnlyControl,
|
|
16
|
+
abort: rejectViewOnlyControl,
|
|
17
|
+
startNewSession: rejectViewOnlyControl,
|
|
18
|
+
switchPiSession: rejectViewOnlyControl,
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
...baseRuntime,
|
|
22
|
+
project,
|
|
23
|
+
cwd: projectRoot,
|
|
24
|
+
currentProject: project.displayName,
|
|
25
|
+
projectMarchDir: join(projectRoot, ".march"),
|
|
26
|
+
sessionsRoot: join(projectRoot, ".march", "sessions"),
|
|
27
|
+
sessionState,
|
|
28
|
+
runner,
|
|
29
|
+
turnTask: null,
|
|
30
|
+
viewOnly: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rejectViewOnlyControl() {
|
|
35
|
+
throw new Error("This session is view-only. Use /session and Take over control before sending prompts.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveProjectRoot(project, fallbackRuntime) {
|
|
39
|
+
return project?.rootPath ? resolve(project.rootPath) : fallbackRuntime.cwd;
|
|
40
|
+
}
|