march-cli 0.1.38 → 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/agent/runner.mjs +8 -8
- package/src/agent/runtime/runner-process-factory.mjs +1 -1
- package/src/agent/turn/turn-runner.mjs +4 -4
- package/src/cli/args.mjs +3 -0
- package/src/cli/commands/mode-command.mjs +1 -0
- package/src/cli/commands/registry/slash-command-registry.mjs +2 -3
- package/src/cli/input/keybindings.mjs +2 -0
- package/src/cli/repl-commands.mjs +1 -1
- package/src/cli/repl-loop.mjs +1 -1
- package/src/cli/session/pi-session-switch-command.mjs +11 -11
- package/src/cli/session/session-list-command.mjs +1 -1
- package/src/cli/session/session-source-command.mjs +0 -76
- package/src/cli/startup/app-runtime.mjs +52 -22
- 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 +11 -37
- package/src/cli/workspace/output-router.mjs +62 -36
- package/src/cli/workspace/project-runtime.mjs +2 -0
- package/src/cli/workspace/runtime-session-state.mjs +9 -0
- package/src/cli/workspace/tui-timeline-projection.mjs +179 -0
- package/src/cli/workspace/tui-timeline.mjs +247 -0
- package/src/extensions/lifecycle-adapter.mjs +2 -2
- package/src/main.mjs +7 -1
- package/src/session/sidecar-sync.mjs +3 -17
- package/src/session/sidecar.mjs +40 -41
- package/src/session/state/march-session-state.mjs +165 -0
- package/src/session/state/march-session-sync.mjs +20 -0
- package/src/session/state/march-session-ui-state.mjs +89 -0
- package/src/workspace/session-index.mjs +27 -0
- package/src/workspace/supervisor.mjs +19 -13
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +0 -10
- package/src/cli/session/session-switch-command.mjs +0 -1
- package/src/session/persist.mjs +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadMarchSessionRenderTimeline({ projectMarchDir, sessionId }) {
|
|
16
|
+
const tuiTimeline = loadTuiRenderTimeline({ projectMarchDir, sessionId });
|
|
17
|
+
if (tuiTimeline) return tuiTimeline;
|
|
18
|
+
|
|
19
|
+
const legacyTimeline = loadLegacyCoreRenderTimeline({ projectMarchDir, sessionId });
|
|
20
|
+
if (legacyTimeline) return legacyTimeline;
|
|
21
|
+
|
|
22
|
+
const stored = loadMarchSessionState({ projectMarchDir, sessionId });
|
|
23
|
+
if (!stored) return null;
|
|
24
|
+
return {
|
|
25
|
+
path: stored.path,
|
|
26
|
+
renderTimeline: renderTimelineFromTurns(stored.state.turns ?? []),
|
|
27
|
+
source: "core-turns",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveMarchSessionRenderTimeline({ projectMarchDir, sessionId, renderTimeline }) {
|
|
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(),
|
|
36
|
+
sessionId,
|
|
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 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeSessionRenderTimeline(events) {
|
|
46
|
+
if (!Array.isArray(events)) return [];
|
|
47
|
+
return events
|
|
48
|
+
.filter((event) => typeof event?.method === "string" && Array.isArray(event.args))
|
|
49
|
+
.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
50
|
+
}
|
|
51
|
+
|
|
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 }) {
|
|
65
|
+
try {
|
|
66
|
+
const path = getMarchSessionStatePath(projectMarchDir, sessionId);
|
|
67
|
+
if (!existsSync(path)) return null;
|
|
68
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
69
|
+
const rawTimeline = normalizeSessionRenderTimeline(raw.renderTimeline);
|
|
70
|
+
if (!raw.renderTimelineUpdatedAt && rawTimeline.length === 0) return null;
|
|
71
|
+
return { path, renderTimeline: rawTimeline, source: "legacy-core-render" };
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderTimelineFromTurns(turns) {
|
|
78
|
+
return turns.flatMap((turn) => {
|
|
79
|
+
const events = [];
|
|
80
|
+
if (turn.userMessage) events.push({ method: "writeln", args: [turn.userMessage], at: null });
|
|
81
|
+
if (turn.assistantMessage) {
|
|
82
|
+
events.push({ method: "turnStart", args: [], at: null });
|
|
83
|
+
events.push({ method: "textDelta", args: [turn.assistantMessage], at: null });
|
|
84
|
+
events.push({ method: "assistantReplyEnd", args: [], at: null });
|
|
85
|
+
events.push({ method: "turnEnd", args: [], at: null });
|
|
86
|
+
}
|
|
87
|
+
return events;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { listPiSessionInfos } from "../session/pi-manager.mjs";
|
|
3
|
+
import { listMarchSessionStates } from "../session/state/march-session-state.mjs";
|
|
3
4
|
import { listRegisteredProjects } from "./project-registry.mjs";
|
|
4
5
|
|
|
5
6
|
export async function listWorkspaceSessions({ stateRoot, currentProjectId = null, listSessions = listPiSessionInfos }) {
|
|
@@ -10,6 +11,7 @@ export async function listWorkspaceSessions({ stateRoot, currentProjectId = null
|
|
|
10
11
|
let sessions = [];
|
|
11
12
|
try {
|
|
12
13
|
sessions = await listSessions({ cwd: project.rootPath, projectMarchDir });
|
|
14
|
+
sessions = mergeMarchSessionStates({ projectMarchDir, backendSessions: sessions });
|
|
13
15
|
} catch {
|
|
14
16
|
sessions = [];
|
|
15
17
|
}
|
|
@@ -73,3 +75,28 @@ function formatWorkspaceSessionTime(value) {
|
|
|
73
75
|
if (!value) return "no saved time";
|
|
74
76
|
return String(value).slice(0, 16).replace("T", " ");
|
|
75
77
|
}
|
|
78
|
+
|
|
79
|
+
function mergeMarchSessionStates({ projectMarchDir, backendSessions }) {
|
|
80
|
+
const backendById = new Map(backendSessions.map((session) => [session.id, session]));
|
|
81
|
+
const backendByPath = new Map(backendSessions.map((session) => [session.path, session]));
|
|
82
|
+
const marchSessions = listMarchSessionStates({ projectMarchDir }).map(({ state }) => {
|
|
83
|
+
const backend = state.backend?.type === "pi"
|
|
84
|
+
? backendById.get(state.backend.sessionId) ?? backendByPath.get(state.backend.sessionFile)
|
|
85
|
+
: null;
|
|
86
|
+
return {
|
|
87
|
+
id: state.sessionId,
|
|
88
|
+
path: state.backend?.sessionFile ?? backend?.path ?? null,
|
|
89
|
+
savedAt: state.savedAt,
|
|
90
|
+
createdAt: backend?.createdAt ?? "",
|
|
91
|
+
cwd: state.cwd,
|
|
92
|
+
name: state.sessionName || backend?.name || "",
|
|
93
|
+
turnCount: state.turns?.length ?? backend?.turnCount ?? 0,
|
|
94
|
+
firstMessage: state.turns?.[0]?.userMessage ?? backend?.firstMessage ?? "",
|
|
95
|
+
parentSessionPath: backend?.parentSessionPath ?? null,
|
|
96
|
+
backend,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
const seenBackendIds = new Set(marchSessions.map((session) => session.backend?.id).filter(Boolean));
|
|
100
|
+
const legacyBackendSessions = backendSessions.filter((session) => !seenBackendIds.has(session.id));
|
|
101
|
+
return [...marchSessions, ...legacyBackendSessions];
|
|
102
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { loadMarchSessionStateForPiBackend } from "../session/state/march-session-state.mjs";
|
|
3
3
|
|
|
4
4
|
export function createWorkspaceSessionSupervisor({ initialRuntime, createProjectRuntime, viewSessionState = initialRuntime?.sessionState, onActivate = null }) {
|
|
5
5
|
if (!initialRuntime?.project?.projectId) throw new Error("initial workspace runtime is missing project metadata");
|
|
@@ -85,7 +85,7 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
85
85
|
if (!result?.cancelled && result?.sessionId) syncSessionState(active, result.sessionId);
|
|
86
86
|
rememberRuntime(active);
|
|
87
87
|
mirrorSessionState(viewSessionState, active.sessionState);
|
|
88
|
-
onActivate?.({ projectId: active.project.projectId, sessionId: getRuntimeSessionId(active), runtime: active });
|
|
88
|
+
onActivate?.({ projectId: active.project.projectId, sessionId: getRuntimeSessionId(active), runtime: active, restoreState: null });
|
|
89
89
|
return { runtime: active, result };
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -96,16 +96,18 @@ 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
|
-
|
|
100
|
-
|
|
99
|
+
const targetSessionId = session?.id ?? null;
|
|
100
|
+
let restoreState = null;
|
|
101
|
+
if (session?.path && getRuntimeSessionId(runtime) !== targetSessionId) {
|
|
102
|
+
restoreState = loadWorkspaceMarchSessionState({ runtime, session });
|
|
101
103
|
await runtime.runner.switchPiSession(session.path, restoreState);
|
|
102
|
-
syncSessionState(runtime, session.id);
|
|
103
104
|
}
|
|
105
|
+
if (targetSessionId) syncSessionState(runtime, targetSessionId);
|
|
104
106
|
|
|
105
107
|
active = runtime;
|
|
106
108
|
rememberRuntime(runtime);
|
|
107
109
|
mirrorSessionState(viewSessionState, runtime.sessionState);
|
|
108
|
-
onActivate?.({ projectId: runtime.project.projectId, sessionId: getRuntimeSessionId(runtime), runtime });
|
|
110
|
+
onActivate?.({ projectId: runtime.project.projectId, sessionId: getRuntimeSessionId(runtime), runtime, restoreState });
|
|
109
111
|
return active;
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -142,17 +144,21 @@ export function createWorkspaceSessionSupervisor({ initialRuntime, createProject
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
function loadWorkspaceMarchSessionState({ runtime, session }) {
|
|
148
|
+
const stored = loadMarchSessionStateForPiBackend({
|
|
149
|
+
projectMarchDir: runtime.projectMarchDir,
|
|
150
|
+
sessionId: session.id,
|
|
151
|
+
sessionRef: session.path,
|
|
152
|
+
});
|
|
153
|
+
if (!stored) throw new Error(`March session state not found for ${session.id}; refusing partial resume`);
|
|
154
|
+
if (stored.state.cwd && stored.state.cwd !== runtime.runner.engine.cwd) {
|
|
155
|
+
throw new Error(`March session state cwd mismatch for ${session.id}: ${stored.state.cwd}`);
|
|
150
156
|
}
|
|
151
|
-
return
|
|
157
|
+
return stored.state;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
function getRuntimeSessionId(runtime) {
|
|
155
|
-
return runtime.
|
|
161
|
+
return runtime.sessionState?.sessionId ?? runtime.runner.getSessionStats?.()?.sessionId ?? null;
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
function syncSessionState(runtime, sessionId) {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export async function createSidecarWriteFailure({ runtimeHost, sourceSessionFile, action, cause }) {
|
|
2
|
-
const causeMessage = cause?.message ?? String(cause);
|
|
3
|
-
const baseMessage = `failed to write pi session sidecar after ${action}: ${causeMessage}`;
|
|
4
|
-
try {
|
|
5
|
-
await runtimeHost.switchSession(sourceSessionFile);
|
|
6
|
-
return new Error(`${baseMessage}; rolled back to source session`);
|
|
7
|
-
} catch (rollbackErr) {
|
|
8
|
-
return new Error(`${baseMessage}; rollback failed: ${rollbackErr.message}`);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// Legacy session switch command removed. Use /session to restore previous pi sessions.
|
package/src/session/persist.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// Legacy session persistence removed. All sessions use pi JSONL format.
|