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
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
|
}
|
|
@@ -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",
|
|
@@ -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
|
+
onPersistRenderTimeline: 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 }) {
|
|
217
|
+
if (!sessionId) 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,16 @@ 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
|
+
}
|
|
230
261
|
|
|
231
|
-
async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor,
|
|
262
|
+
async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, ui }) {
|
|
232
263
|
if (activation?.type !== "workspace-session" || !activation.projectId) return;
|
|
233
264
|
if (!workspaceSupervisor) throw new Error("workspace supervisor is not ready");
|
|
234
265
|
const projects = await listWorkspaceSessions({ stateRoot, currentProjectId: workspaceSupervisor.getActive?.()?.project?.projectId ?? null });
|
|
@@ -237,6 +268,5 @@ async function handleNotificationActivation({ activation, stateRoot, workspaceSu
|
|
|
237
268
|
projectId: activation.projectId,
|
|
238
269
|
sessionId: activation.sessionId,
|
|
239
270
|
});
|
|
240
|
-
outputRouter?.replayBufferedCalls?.(activation.projectId, activation.sessionId);
|
|
241
271
|
ui.writeln(`Activated session from notification: ${runtime.project.displayName}`);
|
|
242
272
|
}
|
|
@@ -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) => {
|