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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/agent/runner.mjs +8 -8
  3. package/src/agent/runtime/runner-process-factory.mjs +1 -1
  4. package/src/agent/turn/turn-runner.mjs +4 -4
  5. package/src/cli/args.mjs +3 -0
  6. package/src/cli/commands/mode-command.mjs +1 -0
  7. package/src/cli/commands/registry/slash-command-registry.mjs +2 -3
  8. package/src/cli/input/keybindings.mjs +2 -0
  9. package/src/cli/repl-commands.mjs +1 -1
  10. package/src/cli/repl-loop.mjs +1 -1
  11. package/src/cli/session/pi-session-switch-command.mjs +11 -11
  12. package/src/cli/session/session-list-command.mjs +1 -1
  13. package/src/cli/session/session-source-command.mjs +0 -76
  14. package/src/cli/startup/app-runtime.mjs +52 -22
  15. package/src/cli/tui/output/timeline-block-restore.mjs +45 -0
  16. package/src/cli/tui/output-buffer.mjs +5 -0
  17. package/src/cli/tui/tui-input-controller.mjs +16 -0
  18. package/src/cli/ui.mjs +6 -2
  19. package/src/cli/workspace/command.mjs +11 -37
  20. package/src/cli/workspace/output-router.mjs +62 -36
  21. package/src/cli/workspace/project-runtime.mjs +2 -0
  22. package/src/cli/workspace/runtime-session-state.mjs +9 -0
  23. package/src/cli/workspace/tui-timeline-projection.mjs +179 -0
  24. package/src/cli/workspace/tui-timeline.mjs +247 -0
  25. package/src/extensions/lifecycle-adapter.mjs +2 -2
  26. package/src/main.mjs +7 -1
  27. package/src/session/sidecar-sync.mjs +3 -17
  28. package/src/session/sidecar.mjs +40 -41
  29. package/src/session/state/march-session-state.mjs +165 -0
  30. package/src/session/state/march-session-sync.mjs +20 -0
  31. package/src/session/state/march-session-ui-state.mjs +89 -0
  32. package/src/workspace/session-index.mjs +27 -0
  33. package/src/workspace/supervisor.mjs +19 -13
  34. package/src/agent/pi-session/pi-session-sidecar-failure.mjs +0 -10
  35. package/src/cli/session/session-switch-command.mjs +0 -1
  36. package/src/session/persist.mjs +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -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 { syncPiSessionSidecar } from "../session/sidecar-sync.mjs";
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, syncPiSidecar = false, 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 }) {
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
- syncCurrentPiSidecar,
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
- syncCurrentPiSidecar();
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
- syncCurrentPiSidecar();
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 syncCurrentPiSidecar() {
260
- return syncPiSessionSidecar({
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
- syncPiSidecar: true,
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
- syncCurrentPiSidecar,
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
- syncCurrentPiSidecar,
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, syncCurrentPiSidecar, autoNameSession, recordHistory }) {
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
- syncCurrentPiSidecar();
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
  }
@@ -15,6 +15,7 @@ export function handleModeCommand(command, { modeState } = {}) {
15
15
 
16
16
  if (command.type === "set") {
17
17
  modeState.set(command.mode);
18
+ return [];
18
19
  }
19
20
 
20
21
  return [`Mode: ${formatModeLabel(modeState.get())}`];
@@ -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) => (trimmed === "/session" || trimmed === "/save") ? { parsed: { trimmed } } : null,
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 previous sessions and restores the selected one.",
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 Restore a previous 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",
@@ -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 /switch to start or inspect another session.");
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 { loadPiSessionSidecar } from "../../session/sidecar.mjs";
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 sidecar;
15
+ let stored;
16
16
  try {
17
- sidecar = loadPiSessionSidecar({ projectMarchDir, sessionRef: session.path });
17
+ stored = loadMarchSessionStateForPiBackend({ projectMarchDir, sessionId: session.id, sessionRef: session.path });
18
18
  } catch (err) {
19
- return [`Error: pi session sidecar is invalid for ${session.id}: ${err.message}`];
19
+ return [`Error: March session state is invalid for ${session.id}: ${err.message}`];
20
20
  }
21
- if (!sidecar) {
22
- return [`Error: pi session sidecar not found for ${session.id}; refusing partial resume`];
21
+ if (!stored) {
22
+ return [`Error: March session state not found for ${session.id}; refusing partial resume`];
23
23
  }
24
- if (sidecar.state.cwd && sidecar.state.cwd !== runner.engine.cwd) {
25
- return [`Error: pi session sidecar cwd mismatch for ${session.id}: ${sidecar.state.cwd}`];
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(sidecar.state);
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(sidecarState) {
40
- return { ...sidecarState };
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; use /session to restore a previous session)");
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({ ui, activeProjectId: currentProjectInfo.projectId, activeSessionId: sessionState.sessionId });
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, outputRouter, ui }).catch((err) => ui.writeln(`Notification activation failed: ${err.message}`));
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) => createWorkspaceProjectRuntime({
152
- project,
153
- args,
154
- config,
155
- stateRoot,
156
- memoryRoot,
157
- profilePaths,
158
- createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot }),
159
- provider,
160
- serviceTier,
161
- model,
162
- remoteMemorySources,
163
- createUi: (runtimeSessionState) => outputRouter.createProjectUi(project.projectId, () => runtimeSessionState.sessionId),
164
- refreshStatusBar: (...args) => refreshStatusBar?.(...args),
165
- onNotificationActivation,
166
- }),
167
- onActivate: ({ projectId, sessionId }) => outputRouter.setActiveSession(projectId, sessionId),
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, outputRouter, ui }) {
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) => {