march-cli 0.1.38 → 0.1.39
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/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 +56 -22
- package/src/cli/workspace/command.mjs +11 -37
- package/src/cli/workspace/output-router.mjs +52 -33
- package/src/cli/workspace/project-runtime.mjs +2 -0
- package/src/cli/workspace/runtime-session-state.mjs +9 -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 +175 -0
- package/src/session/state/march-session-sync.mjs +20 -0
- package/src/session/state/march-session-ui-state.mjs +60 -0
- package/src/workspace/session-index.mjs +27 -0
- package/src/workspace/supervisor.mjs +16 -10
- 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
package/package.json
CHANGED
package/src/agent/runner.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createAgentSession, ModelRegistry, SettingsManager } from "@earendil-wo
|
|
|
2
2
|
import { createMarchAuthStorage } from "../auth/storage.mjs";
|
|
3
3
|
import { ContextEngine } from "../context/engine.mjs";
|
|
4
4
|
import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { syncMarchSessionState } from "../session/state/march-session-sync.mjs";
|
|
6
6
|
import { LspService } from "../lsp/service.mjs";
|
|
7
7
|
import { formatLspServiceEvent } from "../lsp/status-message.mjs";
|
|
8
8
|
import { estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
|
|
@@ -30,7 +30,7 @@ import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/ru
|
|
|
30
30
|
export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
|
|
31
31
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
32
32
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
33
|
-
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null,
|
|
33
|
+
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncMarchSessionState: syncMarchSessionStateEnabled = false, syncPiSidecar = syncMarchSessionStateEnabled, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {}, notificationContext = null }) {
|
|
34
34
|
installRunnerProcessGuards();
|
|
35
35
|
if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
|
|
36
36
|
const authConfig = authStorage ? { authStorage, hasAuth: true } : createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
|
|
@@ -125,7 +125,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
125
125
|
setModelCallKind: (kind) => { currentModelCallKind = kind; },
|
|
126
126
|
logger: turnLog.logger,
|
|
127
127
|
setPhase: turnLog.setPhase,
|
|
128
|
-
|
|
128
|
+
syncCurrentMarchSessionState,
|
|
129
129
|
autoNameSession,
|
|
130
130
|
contextMode,
|
|
131
131
|
recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
|
|
@@ -206,14 +206,14 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
206
206
|
const activeSession = sessionBinding.get();
|
|
207
207
|
activeSession.setSessionName?.(name);
|
|
208
208
|
engine.setSessionName(name);
|
|
209
|
-
|
|
209
|
+
syncCurrentMarchSessionState();
|
|
210
210
|
return engine.sessionName;
|
|
211
211
|
},
|
|
212
212
|
canSwitchPiSession() { return Boolean(runtimeHost); },
|
|
213
213
|
async startNewSession() {
|
|
214
214
|
if (!runtimeHost) throw new Error("pi runtime host is not enabled");
|
|
215
215
|
nextTurnContextMode = "rebuild";
|
|
216
|
-
|
|
216
|
+
syncCurrentMarchSessionState();
|
|
217
217
|
const result = await runtimeHost.newSession();
|
|
218
218
|
if (result?.cancelled) return { cancelled: true };
|
|
219
219
|
engine.restoreSession({}, [], { replace: true });
|
|
@@ -256,9 +256,9 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
256
256
|
},
|
|
257
257
|
};
|
|
258
258
|
return runner;
|
|
259
|
-
function
|
|
260
|
-
return
|
|
261
|
-
enabled: syncPiSidecar, projectMarchDir, engine,
|
|
259
|
+
function syncCurrentMarchSessionState() {
|
|
260
|
+
return syncMarchSessionState({
|
|
261
|
+
enabled: syncPiSidecar || syncMarchSessionStateEnabled, projectMarchDir, engine,
|
|
262
262
|
sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost),
|
|
263
263
|
});
|
|
264
264
|
}
|
|
@@ -72,7 +72,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
|
|
|
72
72
|
enabled: true,
|
|
73
73
|
}),
|
|
74
74
|
useRuntimeHost: true,
|
|
75
|
-
|
|
75
|
+
syncMarchSessionState: true,
|
|
76
76
|
lifecycleHooks: options.lifecycleHooks ?? [],
|
|
77
77
|
lifecycleDiagnostics: options.lifecycleDiagnostics ?? [],
|
|
78
78
|
authStorage: d.createMarchAuthStorage({
|
|
@@ -14,7 +14,7 @@ export async function runRunnerTurn({
|
|
|
14
14
|
setModelCallKind,
|
|
15
15
|
logger = null,
|
|
16
16
|
setPhase = null,
|
|
17
|
-
|
|
17
|
+
syncCurrentMarchSessionState,
|
|
18
18
|
autoNameSession,
|
|
19
19
|
contextMode = "rebuild",
|
|
20
20
|
recordHistory = null,
|
|
@@ -79,7 +79,7 @@ export async function runRunnerTurn({
|
|
|
79
79
|
ui,
|
|
80
80
|
turnState,
|
|
81
81
|
midTurnRecallHints,
|
|
82
|
-
|
|
82
|
+
syncCurrentMarchSessionState,
|
|
83
83
|
autoNameSession,
|
|
84
84
|
recordHistory,
|
|
85
85
|
});
|
|
@@ -129,7 +129,7 @@ function logSessionEvent(logger, event) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints,
|
|
132
|
+
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
|
|
133
133
|
closeAssistantReply({ ui, state: turnState });
|
|
134
134
|
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
135
135
|
engine.setPendingAssistantRecallHints?.(assistantRecallHints);
|
|
@@ -145,7 +145,7 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
|
|
|
145
145
|
recordHistory?.({ ...turn, thinking: assistantThinkingText(turnState), toolCalls: turnState.toolCalls });
|
|
146
146
|
|
|
147
147
|
autoNameSession?.();
|
|
148
|
-
|
|
148
|
+
syncCurrentMarchSessionState();
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
function flushAssistantRecall({ memoryStore, engine, turnState, currentProject }) {
|
package/src/cli/args.mjs
CHANGED
|
@@ -26,6 +26,7 @@ export function parseCliArgs(argv) {
|
|
|
26
26
|
workspace: { type: "string" },
|
|
27
27
|
dev: { type: "boolean" },
|
|
28
28
|
help: { type: "boolean", short: "h" },
|
|
29
|
+
version: { type: "boolean", short: "v" },
|
|
29
30
|
},
|
|
30
31
|
allowPositionals: true,
|
|
31
32
|
});
|
|
@@ -55,6 +56,7 @@ export function parseCliArgs(argv) {
|
|
|
55
56
|
workspace: values.workspace ?? null,
|
|
56
57
|
dev: values.dev ?? false,
|
|
57
58
|
help: values.help ?? false,
|
|
59
|
+
version: values.version ?? false,
|
|
58
60
|
prompt: commandName ? "" : positionals.join(" "),
|
|
59
61
|
};
|
|
60
62
|
}
|
|
@@ -105,5 +107,6 @@ Options:
|
|
|
105
107
|
--name <name> With memory serve/add, remote memory source name
|
|
106
108
|
--foreground With memory serve, run server in current process
|
|
107
109
|
-h, --help Show this help
|
|
110
|
+
-v, --version Show the March CLI version
|
|
108
111
|
`);
|
|
109
112
|
}
|
|
@@ -227,10 +227,9 @@ function parsedCommand({ names, metadata, parse, run }) {
|
|
|
227
227
|
function sessionSourceCommand() {
|
|
228
228
|
return {
|
|
229
229
|
metadata: [
|
|
230
|
-
{ name: "session", description: "Open previous session selector" },
|
|
231
230
|
{ name: "save", description: "Show auto-save status" },
|
|
232
231
|
],
|
|
233
|
-
match: (trimmed) =>
|
|
232
|
+
match: (trimmed) => trimmed === "/save" ? { parsed: { trimmed } } : null,
|
|
234
233
|
run: async (ctx, { trimmed }) => handleSessionSourceCommand(trimmed, ctx),
|
|
235
234
|
};
|
|
236
235
|
}
|
|
@@ -284,7 +283,7 @@ function writeLines(ui, lines) {
|
|
|
284
283
|
export function formatHelpLines() {
|
|
285
284
|
return [
|
|
286
285
|
`Commands: ${getHelpCommandSyntaxes().join(", ")}`,
|
|
287
|
-
"Sessions: /session opens
|
|
286
|
+
"Sessions: /session opens the workspace session selector.",
|
|
288
287
|
"Shortcuts: Tab = toggle Do/Discuss, Esc = abort turn, Ctrl+C = abort turn / press twice to exit when idle, Ctrl+O = toggle tool output, Alt+S = shell pane, Alt+N = next shell, Alt+K/J = shell scroll, PageUp/PageDown = output scroll, Ctrl+G = external editor, Shift+Tab = thinking selector, Ctrl+T = thinking selector, Ctrl+L = model selector",
|
|
289
288
|
];
|
|
290
289
|
}
|
|
@@ -57,7 +57,7 @@ export function formatHotkeysPanel(keybindings = DEFAULT_KEYBINDINGS, diagnostic
|
|
|
57
57
|
...formatKeybindingDiagnostics(diagnostics),
|
|
58
58
|
"Input prefixes:",
|
|
59
59
|
" / Slash command autocomplete",
|
|
60
|
-
" /session
|
|
60
|
+
" /session Open workspace session selector",
|
|
61
61
|
" /thinking Choose or list/set thinking level",
|
|
62
62
|
" @ File path autocomplete",
|
|
63
63
|
" ! cmd Run local shell command without sending to the model",
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -105,7 +105,7 @@ export async function runInteractiveRepl({
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (turnActive.turnTask) {
|
|
108
|
-
ui.writeln("This session is still running. Use /
|
|
108
|
+
ui.writeln("This session is still running. Use /session to start or inspect another session.");
|
|
109
109
|
continue;
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadMarchSessionStateForPiBackend } from "../../session/state/march-session-state.mjs";
|
|
2
2
|
|
|
3
3
|
export async function resumePiSessionById(id, { runner, sessions, projectMarchDir }) {
|
|
4
4
|
if (!runner.canSwitchPiSession?.()) {
|
|
@@ -12,21 +12,21 @@ export async function resumePiSessionById(id, { runner, sessions, projectMarchDi
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const session = matches[0];
|
|
15
|
-
let
|
|
15
|
+
let stored;
|
|
16
16
|
try {
|
|
17
|
-
|
|
17
|
+
stored = loadMarchSessionStateForPiBackend({ projectMarchDir, sessionId: session.id, sessionRef: session.path });
|
|
18
18
|
} catch (err) {
|
|
19
|
-
return [`Error:
|
|
19
|
+
return [`Error: March session state is invalid for ${session.id}: ${err.message}`];
|
|
20
20
|
}
|
|
21
|
-
if (!
|
|
22
|
-
return [`Error:
|
|
21
|
+
if (!stored) {
|
|
22
|
+
return [`Error: March session state not found for ${session.id}; refusing partial resume`];
|
|
23
23
|
}
|
|
24
|
-
if (
|
|
25
|
-
return [`Error:
|
|
24
|
+
if (stored.state.cwd && stored.state.cwd !== runner.engine.cwd) {
|
|
25
|
+
return [`Error: March session state cwd mismatch for ${session.id}: ${stored.state.cwd}`];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
let result;
|
|
29
|
-
const restoreState = toContextSessionState(
|
|
29
|
+
const restoreState = toContextSessionState(stored.state);
|
|
30
30
|
try {
|
|
31
31
|
result = await runner.switchPiSession(session.path, restoreState);
|
|
32
32
|
} catch (err) {
|
|
@@ -36,6 +36,6 @@ export async function resumePiSessionById(id, { runner, sessions, projectMarchDi
|
|
|
36
36
|
return [`Resumed pi session: ${session.id}`];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function toContextSessionState(
|
|
40
|
-
return { ...
|
|
39
|
+
function toContextSessionState(sessionState) {
|
|
40
|
+
return { ...sessionState };
|
|
41
41
|
}
|
|
@@ -23,7 +23,7 @@ export function formatPiSessionList(sessions) {
|
|
|
23
23
|
const savedAt = session.savedAt?.slice(0, 19) ?? "?";
|
|
24
24
|
return ` ${session.id} ${session.turnCount}m ${savedAt} ${label}`;
|
|
25
25
|
});
|
|
26
|
-
lines.push("(pi JSONL session files
|
|
26
|
+
lines.push("(pi JSONL session files)");
|
|
27
27
|
return lines;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import { listPiSessionInfos } from "../../session/pi-manager.mjs";
|
|
2
|
-
import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
|
|
3
|
-
import { resumePiSessionById } from "./pi-session-switch-command.mjs";
|
|
4
|
-
|
|
5
1
|
export async function handleSessionSourceCommand(trimmed, {
|
|
6
2
|
ui,
|
|
7
3
|
runner,
|
|
8
4
|
sessionState,
|
|
9
|
-
sessionsRoot,
|
|
10
|
-
projectMarchDir,
|
|
11
5
|
}) {
|
|
12
6
|
if (trimmed === "/save") {
|
|
13
7
|
const stats = runner.getSessionStats?.();
|
|
@@ -15,75 +9,5 @@ export async function handleSessionSourceCommand(trimmed, {
|
|
|
15
9
|
return { handled: true };
|
|
16
10
|
}
|
|
17
11
|
|
|
18
|
-
if (trimmed === "/session") {
|
|
19
|
-
const sessions = await listPiSessionInfos({
|
|
20
|
-
cwd: runner.engine.cwd,
|
|
21
|
-
projectMarchDir,
|
|
22
|
-
});
|
|
23
|
-
if (sessions.length === 0) {
|
|
24
|
-
ui.writeln("No previous sessions.");
|
|
25
|
-
return { handled: true };
|
|
26
|
-
}
|
|
27
|
-
if (!ui.selectList) {
|
|
28
|
-
ui.writeln("Session selector is only available in TUI.");
|
|
29
|
-
return { handled: true };
|
|
30
|
-
}
|
|
31
|
-
const currentSessionId = runner.getSessionStats?.().sessionId ?? null;
|
|
32
|
-
const item = await ui.selectList({
|
|
33
|
-
items: buildSessionSelectItems(sessions, currentSessionId),
|
|
34
|
-
selectedIndex: Math.max(0, sessions.findIndex((session) => session.id === currentSessionId)),
|
|
35
|
-
width: 72,
|
|
36
|
-
suppressInitialConfirm: true,
|
|
37
|
-
searchable: true,
|
|
38
|
-
getSearchText: sessionSelectSearchText,
|
|
39
|
-
});
|
|
40
|
-
if (!item) {
|
|
41
|
-
ui.writeln("Session unchanged.");
|
|
42
|
-
return { handled: true };
|
|
43
|
-
}
|
|
44
|
-
const lines = await resumePiSessionById(item.session.id, { runner, sessions, projectMarchDir });
|
|
45
|
-
if (isResumeSuccess(lines)) restoreTranscriptFromSession(item.session, ui);
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
ui.writeln(line);
|
|
48
|
-
}
|
|
49
|
-
return { handled: true };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
12
|
return { handled: false };
|
|
53
13
|
}
|
|
54
|
-
|
|
55
|
-
export function buildSessionSelectItems(sessions, currentSessionId = null) {
|
|
56
|
-
return sessions.map((session) => {
|
|
57
|
-
const label = session.name || session.firstMessage || "(no messages)";
|
|
58
|
-
const savedAt = formatSessionSelectTime(session.savedAt);
|
|
59
|
-
return {
|
|
60
|
-
value: session.id,
|
|
61
|
-
label,
|
|
62
|
-
description: savedAt,
|
|
63
|
-
session,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function sessionSelectSearchText(item) {
|
|
69
|
-
const session = item?.session;
|
|
70
|
-
return `${item?.label ?? ""} ${item?.description ?? ""} ${session?.id ?? ""} ${session?.name ?? ""} ${session?.firstMessage ?? ""} ${session?.turnCount ?? ""}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function restoreTranscriptFromSession(session, ui) {
|
|
74
|
-
if (typeof ui.restoreTranscript !== "function") return;
|
|
75
|
-
try {
|
|
76
|
-
ui.restoreTranscript(loadPiSessionTranscriptTurns(session.path));
|
|
77
|
-
} catch (err) {
|
|
78
|
-
ui.writeln(`Warning: failed to restore session transcript: ${err.message}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function isResumeSuccess(lines) {
|
|
83
|
-
return Array.isArray(lines) && lines.some((line) => String(line).startsWith("Resumed pi session:"));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function formatSessionSelectTime(value) {
|
|
87
|
-
if (!value) return "?";
|
|
88
|
-
return String(value).slice(0, 16).replace("T", " ");
|
|
89
|
-
}
|
|
@@ -24,6 +24,8 @@ import { listWorkspaceSessions } from "../../workspace/session-index.mjs";
|
|
|
24
24
|
import { createWorkspaceSessionSupervisor } from "../../workspace/supervisor.mjs";
|
|
25
25
|
import { createWorkspaceProjectRuntime } from "../workspace/project-runtime.mjs";
|
|
26
26
|
import { createWorkspaceOutputRouter } from "../workspace/output-router.mjs";
|
|
27
|
+
import { syncRuntimeSessionStateFromRunner } from "../workspace/runtime-session-state.mjs";
|
|
28
|
+
import { loadMarchSessionRenderTimeline, saveMarchSessionRenderTimeline } from "../../session/state/march-session-ui-state.mjs";
|
|
27
29
|
|
|
28
30
|
export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot } = {}) {
|
|
29
31
|
if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
|
|
@@ -62,6 +64,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
62
64
|
const modeState = createModeState();
|
|
63
65
|
const namespace = loadOrCreateProjectId(projectMarchDir);
|
|
64
66
|
const currentProjectInfo = registerProject({ stateRoot, rootPath: cwd });
|
|
67
|
+
const projectMarchDirs = new Map([[currentProjectInfo.projectId, projectMarchDir]]);
|
|
65
68
|
const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
|
|
66
69
|
const profilePaths = defaultProfilePaths();
|
|
67
70
|
ensureProfileFiles(profilePaths);
|
|
@@ -87,7 +90,12 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
87
90
|
shellRuntime,
|
|
88
91
|
historyStore: inputHistoryStore,
|
|
89
92
|
});
|
|
90
|
-
const outputRouter = createWorkspaceOutputRouter({
|
|
93
|
+
const outputRouter = createWorkspaceOutputRouter({
|
|
94
|
+
ui,
|
|
95
|
+
activeProjectId: currentProjectInfo.projectId,
|
|
96
|
+
activeSessionId: sessionState.sessionId,
|
|
97
|
+
onRenderTimelineChange: persistRenderTimeline,
|
|
98
|
+
});
|
|
91
99
|
const runtimeUi = outputRouter.createProjectUi(currentProjectInfo.projectId, () => sessionState.sessionId);
|
|
92
100
|
let turnRunning = false;
|
|
93
101
|
let refreshStatusBar = null;
|
|
@@ -115,7 +123,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
115
123
|
let runner;
|
|
116
124
|
let workspaceSupervisor = null;
|
|
117
125
|
const onNotificationActivation = (activation) => {
|
|
118
|
-
handleNotificationActivation({ activation, stateRoot, workspaceSupervisor,
|
|
126
|
+
handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, ui }).catch((err) => ui.writeln(`Notification activation failed: ${err.message}`));
|
|
119
127
|
};
|
|
120
128
|
try {
|
|
121
129
|
runner = await createRuntimeRunner({
|
|
@@ -131,6 +139,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
131
139
|
memoryStore.close?.();
|
|
132
140
|
return { ok: false, code: 1, logger };
|
|
133
141
|
}
|
|
142
|
+
syncRuntimeSessionStateFromRunner(sessionState, runner, sessionsRoot);
|
|
134
143
|
|
|
135
144
|
const initialRuntime = {
|
|
136
145
|
project: currentProjectInfo,
|
|
@@ -148,23 +157,26 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
148
157
|
};
|
|
149
158
|
workspaceSupervisor = createWorkspaceSessionSupervisor({
|
|
150
159
|
initialRuntime,
|
|
151
|
-
createProjectRuntime: (project) =>
|
|
152
|
-
project,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
createProjectRuntime: (project) => {
|
|
161
|
+
projectMarchDirs.set(project.projectId, resolve(project.rootPath, ".march"));
|
|
162
|
+
return createWorkspaceProjectRuntime({
|
|
163
|
+
project,
|
|
164
|
+
args,
|
|
165
|
+
config,
|
|
166
|
+
stateRoot,
|
|
167
|
+
memoryRoot,
|
|
168
|
+
profilePaths,
|
|
169
|
+
createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot }),
|
|
170
|
+
provider,
|
|
171
|
+
serviceTier,
|
|
172
|
+
model,
|
|
173
|
+
remoteMemorySources,
|
|
174
|
+
createUi: (runtimeSessionState) => outputRouter.createProjectUi(project.projectId, () => runtimeSessionState.sessionId),
|
|
175
|
+
refreshStatusBar: (...args) => refreshStatusBar?.(...args),
|
|
176
|
+
onNotificationActivation,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
onActivate: ({ projectId, sessionId }) => outputRouter.setActiveSession(projectId, sessionId, { renderTimeline: loadStoredRenderTimeline(projectMarchDirs.get(projectId), sessionId) }),
|
|
168
180
|
});
|
|
169
181
|
runner = workspaceSupervisor.runner;
|
|
170
182
|
|
|
@@ -198,9 +210,20 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
198
210
|
ui,
|
|
199
211
|
});
|
|
200
212
|
workspaceSupervisor.refreshActiveRuntime();
|
|
201
|
-
outputRouter.setActiveSession(currentProjectInfo.projectId, sessionState.sessionId);
|
|
213
|
+
outputRouter.setActiveSession(currentProjectInfo.projectId, sessionState.sessionId, { renderTimeline: loadStoredRenderTimeline(projectMarchDir, sessionState.sessionId) });
|
|
202
214
|
refreshStatusBar();
|
|
203
215
|
|
|
216
|
+
function persistRenderTimeline({ projectId, sessionId, events, event }) {
|
|
217
|
+
if (!sessionId || !shouldPersistRenderEvent(event?.method)) return;
|
|
218
|
+
const routeProjectMarchDir = projectMarchDirs.get(projectId);
|
|
219
|
+
if (!routeProjectMarchDir) return;
|
|
220
|
+
try {
|
|
221
|
+
saveMarchSessionRenderTimeline({ projectMarchDir: routeProjectMarchDir, sessionId, renderTimeline: events });
|
|
222
|
+
} catch {
|
|
223
|
+
// Render persistence is separate from model context; a UI write failure must not corrupt the turn.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
204
227
|
return {
|
|
205
228
|
ok: true,
|
|
206
229
|
args,
|
|
@@ -227,8 +250,20 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
227
250
|
setTurnRunning(value) { turnRunning = value; },
|
|
228
251
|
};
|
|
229
252
|
}
|
|
253
|
+
function loadStoredRenderTimeline(projectMarchDir, sessionId) {
|
|
254
|
+
if (!projectMarchDir || !sessionId) return null;
|
|
255
|
+
try {
|
|
256
|
+
return loadMarchSessionRenderTimeline({ projectMarchDir, sessionId })?.renderTimeline ?? null;
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function shouldPersistRenderEvent(method) {
|
|
263
|
+
return method === "turnEnd" || method === "assistantReplyEnd" || method === "toolEnd" || method === "writeln" || method === "clearOutput";
|
|
264
|
+
}
|
|
230
265
|
|
|
231
|
-
async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor,
|
|
266
|
+
async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, ui }) {
|
|
232
267
|
if (activation?.type !== "workspace-session" || !activation.projectId) return;
|
|
233
268
|
if (!workspaceSupervisor) throw new Error("workspace supervisor is not ready");
|
|
234
269
|
const projects = await listWorkspaceSessions({ stateRoot, currentProjectId: workspaceSupervisor.getActive?.()?.project?.projectId ?? null });
|
|
@@ -237,6 +272,5 @@ async function handleNotificationActivation({ activation, stateRoot, workspaceSu
|
|
|
237
272
|
projectId: activation.projectId,
|
|
238
273
|
sessionId: activation.sessionId,
|
|
239
274
|
});
|
|
240
|
-
outputRouter?.replayBufferedCalls?.(activation.projectId, activation.sessionId);
|
|
241
275
|
ui.writeln(`Activated session from notification: ${runtime.project.displayName}`);
|
|
242
276
|
}
|
|
@@ -3,8 +3,6 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { brightBlack } from "../tui/ui-theme.mjs";
|
|
4
4
|
import { registerProject, listRegisteredProjects } from "../../workspace/project-registry.mjs";
|
|
5
5
|
import { buildWorkspaceSessionSelectItems, listWorkspaceSessions, workspaceSessionSearchText } from "../../workspace/session-index.mjs";
|
|
6
|
-
import { resumePiSessionById } from "../session/pi-session-switch-command.mjs";
|
|
7
|
-
import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
|
|
8
6
|
|
|
9
7
|
export const WORKSPACE_SLASH_COMMANDS = [
|
|
10
8
|
{
|
|
@@ -19,9 +17,9 @@ export const WORKSPACE_SLASH_COMMANDS = [
|
|
|
19
17
|
run: async (ctx, command) => writeLines(ctx.ui, await handleProjectCommand(command, ctx)),
|
|
20
18
|
},
|
|
21
19
|
{
|
|
22
|
-
metadata: [{ name: "
|
|
23
|
-
match: (trimmed) => trimmed === "/
|
|
24
|
-
run:
|
|
20
|
+
metadata: [{ name: "session", description: "Open workspace session selector" }],
|
|
21
|
+
match: (trimmed) => trimmed === "/session" ? { parsed: { type: "session" } } : null,
|
|
22
|
+
run: handleSessionCommand,
|
|
25
23
|
},
|
|
26
24
|
];
|
|
27
25
|
|
|
@@ -45,9 +43,9 @@ export async function handleProjectCommand(command, { stateRoot }) {
|
|
|
45
43
|
return ["Registered projects:", ...projects.map((project) => `- ${project.displayName} ${brightBlack(project.rootPath)}`)];
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
export async function
|
|
46
|
+
export async function handleSessionCommand({ stateRoot, currentProjectId, runner, workspaceSupervisor, workspaceOutputRouter, ui }) {
|
|
49
47
|
if (!stateRoot) {
|
|
50
|
-
ui.writeln("Session
|
|
48
|
+
ui.writeln("Session selector is not available: workspace registry is missing.");
|
|
51
49
|
return { handled: true };
|
|
52
50
|
}
|
|
53
51
|
const projects = await listWorkspaceSessions({ stateRoot, currentProjectId });
|
|
@@ -59,7 +57,7 @@ export async function handleSwitchCommand({ stateRoot, currentProjectId, project
|
|
|
59
57
|
return { handled: true };
|
|
60
58
|
}
|
|
61
59
|
if (!ui.selectList) {
|
|
62
|
-
ui.writeln("Session
|
|
60
|
+
ui.writeln("Session selector is only available in TUI.");
|
|
63
61
|
return { handled: true };
|
|
64
62
|
}
|
|
65
63
|
const selectedIndex = Math.max(0, items.findIndex((item) => item.project.current && item.session?.id === currentSessionId));
|
|
@@ -82,7 +80,6 @@ export async function handleSwitchCommand({ stateRoot, currentProjectId, project
|
|
|
82
80
|
}
|
|
83
81
|
try {
|
|
84
82
|
const { result } = await workspaceSupervisor.startNewWorkspaceSession(item.project);
|
|
85
|
-
ui.restoreTranscript?.([]);
|
|
86
83
|
ui.writeln(`Created session: ${item.project.displayName} / ${result?.sessionId ?? "new session"}`);
|
|
87
84
|
return { handled: true, refreshContextTokens: true, activeChanged: true };
|
|
88
85
|
} catch (err) {
|
|
@@ -93,25 +90,15 @@ export async function handleSwitchCommand({ stateRoot, currentProjectId, project
|
|
|
93
90
|
if (workspaceSupervisor) {
|
|
94
91
|
try {
|
|
95
92
|
await workspaceSupervisor.activateWorkspaceSession({ project: item.project, session: item.session });
|
|
96
|
-
|
|
97
|
-
const replayed = ctxReplayBufferedOutput({ workspaceOutputRouter, projectId: item.project.projectId, sessionId: item.session.id });
|
|
98
|
-
ui.writeln(`Switched to session: ${item.project.displayName} / ${item.session.name || item.session.id}${replayed ? ` (${replayed} buffered events replayed)` : ""}`);
|
|
93
|
+
ctxRenderActiveSession({ workspaceOutputRouter, projectId: item.project.projectId, sessionId: item.session.id });
|
|
99
94
|
return { handled: true, refreshContextTokens: true, activeChanged: true };
|
|
100
95
|
} catch (err) {
|
|
101
96
|
ui.writeln(`Error: ${err.message}`);
|
|
102
97
|
return { handled: true };
|
|
103
98
|
}
|
|
104
99
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
ui.writeln(brightBlack("Cross-project attach requires the workspace supervisor."));
|
|
108
|
-
return { handled: true };
|
|
109
|
-
}
|
|
110
|
-
const sessions = projects.find((project) => project.current)?.sessions ?? [];
|
|
111
|
-
const lines = await resumePiSessionById(item.session.id, { runner, sessions, projectMarchDir });
|
|
112
|
-
if (isResumeSuccess(lines)) restoreTranscriptFromSession(item.session, ui);
|
|
113
|
-
for (const line of lines) ui.writeln(line);
|
|
114
|
-
return { handled: true, refreshContextTokens: isResumeSuccess(lines) };
|
|
100
|
+
ui.writeln("Workspace session activation requires the workspace supervisor.");
|
|
101
|
+
return { handled: true };
|
|
115
102
|
}
|
|
116
103
|
|
|
117
104
|
function annotateWorkspaceItems(items, runtimeSummaries) {
|
|
@@ -124,24 +111,11 @@ function annotateWorkspaceItems(items, runtimeSummaries) {
|
|
|
124
111
|
});
|
|
125
112
|
}
|
|
126
113
|
|
|
127
|
-
function
|
|
128
|
-
return workspaceOutputRouter?.
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function restoreTranscriptFromSession(session, ui) {
|
|
132
|
-
if (typeof ui.restoreTranscript !== "function") return;
|
|
133
|
-
try {
|
|
134
|
-
ui.restoreTranscript(loadPiSessionTranscriptTurns(session.path));
|
|
135
|
-
} catch (err) {
|
|
136
|
-
ui.writeln(`Warning: failed to restore session transcript: ${err.message}`);
|
|
137
|
-
}
|
|
114
|
+
function ctxRenderActiveSession({ workspaceOutputRouter, projectId, sessionId }) {
|
|
115
|
+
return workspaceOutputRouter?.getRenderEventCount?.(projectId, sessionId) ?? 0;
|
|
138
116
|
}
|
|
139
117
|
|
|
140
118
|
function writeLines(ui, lines) {
|
|
141
119
|
for (const line of lines) ui.writeln(line);
|
|
142
120
|
return { handled: true };
|
|
143
121
|
}
|
|
144
|
-
|
|
145
|
-
function isResumeSuccess(lines) {
|
|
146
|
-
return Array.isArray(lines) && lines.some((line) => String(line).startsWith("Resumed pi session:"));
|
|
147
|
-
}
|