march-cli 0.1.37 → 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/runner-utils.mjs +20 -0
- package/src/agent/runner.mjs +16 -17
- package/src/agent/runtime/remote-ui-client.mjs +0 -1
- package/src/agent/runtime/runner-process-client.mjs +2 -0
- package/src/agent/runtime/runner-process-factory.mjs +3 -4
- package/src/agent/runtime/runner-runtime-host.mjs +0 -2
- package/src/agent/runtime/ui-event-bridge.mjs +0 -2
- package/src/agent/session/session-options.mjs +1 -2
- package/src/agent/tools.mjs +2 -23
- package/src/agent/turn/turn-runner.mjs +4 -4
- package/src/cli/args.mjs +3 -3
- package/src/cli/commands/mode-command.mjs +1 -0
- package/src/cli/commands/registry/slash-command-registry.mjs +4 -3
- package/src/cli/fallback-ui.mjs +0 -2
- package/src/cli/input/mode-state.mjs +1 -1
- package/src/cli/repl-commands.mjs +1 -1
- package/src/cli/repl-loop.mjs +67 -19
- 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 +103 -4
- package/src/cli/startup/create-runtime-runner.mjs +2 -1
- package/src/cli/startup/startup-session.mjs +3 -13
- package/src/cli/ui.mjs +0 -6
- package/src/cli/workspace/command.mjs +121 -0
- package/src/cli/workspace/output-router.mjs +127 -0
- package/src/cli/workspace/project-runtime.mjs +94 -0
- package/src/cli/workspace/runtime-session-state.mjs +9 -0
- package/src/config/features.mjs +0 -1
- package/src/extensions/lifecycle-adapter.mjs +3 -3
- package/src/main.mjs +11 -1
- package/src/notification/desktop-notifier.mjs +16 -8
- 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/web-ui/dist/assets/index-BQtl1uQs.css +1 -0
- package/src/web-ui/dist/assets/index-DrlJis_D.js +1845 -0
- package/src/web-ui/dist/index.html +13 -0
- package/src/web-ui/runtime-host.mjs +1 -2
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +2 -10
- package/src/web-ui/src/mockData.ts +1 -8
- package/src/web-ui/src/model.ts +0 -2
- package/src/web-ui/src/runtime/client.ts +0 -1
- package/src/web-ui/src/runtime/runtimeTimeline.ts +1 -3
- package/src/web-ui/src/styles/shell.css +1 -2
- package/src/web-ui/src/timelineAdapter.ts +1 -2
- package/src/workspace/project-id.mjs +14 -0
- package/src/workspace/project-registry.mjs +74 -0
- package/src/workspace/session-index.mjs +102 -0
- package/src/workspace/supervisor.mjs +178 -0
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +0 -10
- package/src/cli/permissions.mjs +0 -103
- package/src/cli/session/session-switch-command.mjs +0 -1
- package/src/cli/tui/permission-request-ui.mjs +0 -18
- package/src/session/persist.mjs +0 -1
|
@@ -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
|
-
}
|
|
@@ -19,6 +19,13 @@ import { defaultProfilePaths, ensureProfileFiles } from "../../context/profiles.
|
|
|
19
19
|
import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
|
|
20
20
|
import { resolveMemoryRoot } from "../../memory/root.mjs";
|
|
21
21
|
import { ensureBrowserDaemon } from "../../browser/client/lifecycle.mjs";
|
|
22
|
+
import { registerProject } from "../../workspace/project-registry.mjs";
|
|
23
|
+
import { listWorkspaceSessions } from "../../workspace/session-index.mjs";
|
|
24
|
+
import { createWorkspaceSessionSupervisor } from "../../workspace/supervisor.mjs";
|
|
25
|
+
import { createWorkspaceProjectRuntime } from "../workspace/project-runtime.mjs";
|
|
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";
|
|
22
29
|
|
|
23
30
|
export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot } = {}) {
|
|
24
31
|
if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
|
|
@@ -56,6 +63,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
56
63
|
const inputHistoryStore = createInputHistoryStore({ path: join(projectMarchDir, "input-history.json") });
|
|
57
64
|
const modeState = createModeState();
|
|
58
65
|
const namespace = loadOrCreateProjectId(projectMarchDir);
|
|
66
|
+
const currentProjectInfo = registerProject({ stateRoot, rootPath: cwd });
|
|
67
|
+
const projectMarchDirs = new Map([[currentProjectInfo.projectId, projectMarchDir]]);
|
|
59
68
|
const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
|
|
60
69
|
const profilePaths = defaultProfilePaths();
|
|
61
70
|
ensureProfileFiles(profilePaths);
|
|
@@ -64,7 +73,6 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
64
73
|
const currentProject = basename(cwd);
|
|
65
74
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
66
75
|
|
|
67
|
-
const permissionMode = args.permissionMode;
|
|
68
76
|
const sessionSource = "pi";
|
|
69
77
|
const sessionsRoot = join(projectMarchDir, "sessions");
|
|
70
78
|
const sessionState = {
|
|
@@ -82,7 +90,13 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
82
90
|
shellRuntime,
|
|
83
91
|
historyStore: inputHistoryStore,
|
|
84
92
|
});
|
|
85
|
-
|
|
93
|
+
const outputRouter = createWorkspaceOutputRouter({
|
|
94
|
+
ui,
|
|
95
|
+
activeProjectId: currentProjectInfo.projectId,
|
|
96
|
+
activeSessionId: sessionState.sessionId,
|
|
97
|
+
onRenderTimelineChange: persistRenderTimeline,
|
|
98
|
+
});
|
|
99
|
+
const runtimeUi = outputRouter.createProjectUi(currentProjectInfo.projectId, () => sessionState.sessionId);
|
|
86
100
|
let turnRunning = false;
|
|
87
101
|
let refreshStatusBar = null;
|
|
88
102
|
const runnerOptions = {
|
|
@@ -98,21 +112,26 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
98
112
|
namespace,
|
|
99
113
|
projectMarchDir,
|
|
100
114
|
extensionPaths,
|
|
101
|
-
permissionMode,
|
|
102
115
|
shellRuntime: Boolean(shellRuntime),
|
|
103
116
|
lifecycleHooks: lifecycleManifests.hooks,
|
|
104
117
|
lifecycleDiagnostics: lifecycleManifests.diagnostics,
|
|
105
118
|
modelContextDumper: { enabled: args.dumpContext, rootDir: contextDumpRoot },
|
|
106
119
|
remoteMemorySources,
|
|
120
|
+
notificationContext: { projectId: currentProjectInfo.projectId },
|
|
107
121
|
};
|
|
108
122
|
|
|
109
123
|
let runner;
|
|
124
|
+
let workspaceSupervisor = null;
|
|
125
|
+
const onNotificationActivation = (activation) => {
|
|
126
|
+
handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, ui }).catch((err) => ui.writeln(`Notification activation failed: ${err.message}`));
|
|
127
|
+
};
|
|
110
128
|
try {
|
|
111
129
|
runner = await createRuntimeRunner({
|
|
112
130
|
runnerOptions,
|
|
113
|
-
ui,
|
|
131
|
+
ui: runtimeUi,
|
|
114
132
|
shellRuntime,
|
|
115
133
|
refreshStatusBar: (...args) => refreshStatusBar?.(...args),
|
|
134
|
+
onNotificationActivation,
|
|
116
135
|
});
|
|
117
136
|
} catch (err) {
|
|
118
137
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
@@ -120,6 +139,46 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
120
139
|
memoryStore.close?.();
|
|
121
140
|
return { ok: false, code: 1, logger };
|
|
122
141
|
}
|
|
142
|
+
syncRuntimeSessionStateFromRunner(sessionState, runner, sessionsRoot);
|
|
143
|
+
|
|
144
|
+
const initialRuntime = {
|
|
145
|
+
project: currentProjectInfo,
|
|
146
|
+
cwd,
|
|
147
|
+
currentProject,
|
|
148
|
+
runner,
|
|
149
|
+
ui: runtimeUi,
|
|
150
|
+
memoryStore,
|
|
151
|
+
sessionState,
|
|
152
|
+
sessionsRoot,
|
|
153
|
+
projectMarchDir,
|
|
154
|
+
extensionPaths,
|
|
155
|
+
keybindingConfig,
|
|
156
|
+
promptTemplateConfig,
|
|
157
|
+
};
|
|
158
|
+
workspaceSupervisor = createWorkspaceSessionSupervisor({
|
|
159
|
+
initialRuntime,
|
|
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) }),
|
|
180
|
+
});
|
|
181
|
+
runner = workspaceSupervisor.runner;
|
|
123
182
|
|
|
124
183
|
refreshStatusBar = createStatusLineUpdater({
|
|
125
184
|
ui,
|
|
@@ -150,16 +209,32 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
150
209
|
projectMarchDir,
|
|
151
210
|
ui,
|
|
152
211
|
});
|
|
212
|
+
workspaceSupervisor.refreshActiveRuntime();
|
|
213
|
+
outputRouter.setActiveSession(currentProjectInfo.projectId, sessionState.sessionId, { renderTimeline: loadStoredRenderTimeline(projectMarchDir, sessionState.sessionId) });
|
|
153
214
|
refreshStatusBar();
|
|
154
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
|
+
|
|
155
227
|
return {
|
|
156
228
|
ok: true,
|
|
157
229
|
args,
|
|
158
230
|
cwd,
|
|
159
231
|
ui,
|
|
160
232
|
runner,
|
|
233
|
+
workspaceSupervisor,
|
|
234
|
+
workspaceOutputRouter: outputRouter,
|
|
161
235
|
memoryStore,
|
|
162
236
|
currentProject,
|
|
237
|
+
currentProjectInfo,
|
|
163
238
|
sessionState,
|
|
164
239
|
sessionsRoot,
|
|
165
240
|
projectMarchDir,
|
|
@@ -175,3 +250,27 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
175
250
|
setTurnRunning(value) { turnRunning = value; },
|
|
176
251
|
};
|
|
177
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
|
+
}
|
|
265
|
+
|
|
266
|
+
async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, ui }) {
|
|
267
|
+
if (activation?.type !== "workspace-session" || !activation.projectId) return;
|
|
268
|
+
if (!workspaceSupervisor) throw new Error("workspace supervisor is not ready");
|
|
269
|
+
const projects = await listWorkspaceSessions({ stateRoot, currentProjectId: workspaceSupervisor.getActive?.()?.project?.projectId ?? null });
|
|
270
|
+
const runtime = await workspaceSupervisor.activateWorkspaceSessionById({
|
|
271
|
+
projects,
|
|
272
|
+
projectId: activation.projectId,
|
|
273
|
+
sessionId: activation.sessionId,
|
|
274
|
+
});
|
|
275
|
+
ui.writeln(`Activated session from notification: ${runtime.project.displayName}`);
|
|
276
|
+
}
|
|
@@ -5,6 +5,7 @@ export async function createRuntimeRunner({
|
|
|
5
5
|
ui,
|
|
6
6
|
shellRuntime,
|
|
7
7
|
refreshStatusBar,
|
|
8
|
+
onNotificationActivation = null,
|
|
8
9
|
} = {}) {
|
|
9
10
|
const onModelPayload = ({ estimatedTokens }) => {
|
|
10
11
|
refreshStatusBar?.({ contextTokens: estimatedTokens });
|
|
@@ -13,7 +14,7 @@ export async function createRuntimeRunner({
|
|
|
13
14
|
refreshStatusBar?.();
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
const { runner } = await createRunnerProcessClient({ runnerOptions, ui, onModelPayload, onLspStatusChange });
|
|
17
|
+
const { runner } = await createRunnerProcessClient({ runnerOptions, ui, onModelPayload, onLspStatusChange, onNotificationActivation });
|
|
17
18
|
runner.shellRuntime ??= shellRuntime;
|
|
18
19
|
return runner;
|
|
19
20
|
}
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { loadOrCreateProjectId } from "../../workspace/project-id.mjs";
|
|
4
2
|
import { listPiSessionInfos } from "../../session/pi-manager.mjs";
|
|
3
|
+
|
|
5
4
|
import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
|
|
6
5
|
import { resumePiSessionById } from "../session/pi-session-switch-command.mjs";
|
|
7
6
|
|
|
8
|
-
export
|
|
9
|
-
if (!existsSync(projectMarchDir)) mkdirSync(projectMarchDir, { recursive: true });
|
|
10
|
-
const idFile = resolve(projectMarchDir, "project-id");
|
|
11
|
-
if (existsSync(idFile)) {
|
|
12
|
-
return readFileSync(idFile, "utf8").trim();
|
|
13
|
-
}
|
|
14
|
-
const id = randomUUID();
|
|
15
|
-
writeFileSync(idFile, id, "utf8");
|
|
16
|
-
return id;
|
|
17
|
-
}
|
|
7
|
+
export { loadOrCreateProjectId };
|
|
18
8
|
|
|
19
9
|
export async function resumeStartupSession({
|
|
20
10
|
resumeId,
|
package/src/cli/ui.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { buildMarchCommands, MarchAutocompleteProvider } from "./input/autocompl
|
|
|
5
5
|
import { createJsonUI, createPlainUI } from "./fallback-ui.mjs";
|
|
6
6
|
import { createKeybindingDispatcher } from "./input/keybinding-dispatch.mjs";
|
|
7
7
|
import { OutputBuffer } from "./tui/output-buffer.mjs";
|
|
8
|
-
import { requestToolPermission } from "./tui/permission-request-ui.mjs";
|
|
9
8
|
import { runTuiExternalEditor } from "./tui/editor/external-editor-runner.mjs";
|
|
10
9
|
import { createRetryStatusController } from "./tui/status/retry-status.mjs";
|
|
11
10
|
import { createShellDrawerControls } from "./shell/shell-drawer-controls.mjs";
|
|
@@ -248,11 +247,6 @@ export function createTuiUI({
|
|
|
248
247
|
requestRender();
|
|
249
248
|
},
|
|
250
249
|
|
|
251
|
-
requestPermission: async ({ toolName, params, category }) => {
|
|
252
|
-
ensureStarted();
|
|
253
|
-
spinnerStatus.stop();
|
|
254
|
-
return requestToolPermission({ toolName, params, category, output, selectList, requestRender });
|
|
255
|
-
},
|
|
256
250
|
|
|
257
251
|
setEscapeHandler: (fn) => { onEscapeHandler = fn; },
|
|
258
252
|
setCtrlCHandler: (fn) => { onCtrlCHandler = fn; },
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { brightBlack } from "../tui/ui-theme.mjs";
|
|
4
|
+
import { registerProject, listRegisteredProjects } from "../../workspace/project-registry.mjs";
|
|
5
|
+
import { buildWorkspaceSessionSelectItems, listWorkspaceSessions, workspaceSessionSearchText } from "../../workspace/session-index.mjs";
|
|
6
|
+
|
|
7
|
+
export const WORKSPACE_SLASH_COMMANDS = [
|
|
8
|
+
{
|
|
9
|
+
metadata: [
|
|
10
|
+
{ name: "project", description: "List registered projects" },
|
|
11
|
+
{ name: "project add", helpSyntax: "project add <path>", description: "Register a project root" },
|
|
12
|
+
],
|
|
13
|
+
match: (trimmed) => {
|
|
14
|
+
const parsed = parseProjectCommand(trimmed);
|
|
15
|
+
return parsed.type === "none" ? null : { parsed };
|
|
16
|
+
},
|
|
17
|
+
run: async (ctx, command) => writeLines(ctx.ui, await handleProjectCommand(command, ctx)),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
metadata: [{ name: "session", description: "Open workspace session selector" }],
|
|
21
|
+
match: (trimmed) => trimmed === "/session" ? { parsed: { type: "session" } } : null,
|
|
22
|
+
run: handleSessionCommand,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function parseProjectCommand(trimmed) {
|
|
27
|
+
if (trimmed === "/project" || trimmed === "/project list") return { type: "list" };
|
|
28
|
+
if (trimmed.startsWith("/project add ")) return { type: "add", path: trimmed.slice("/project add ".length).trim() };
|
|
29
|
+
return { type: "none" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function handleProjectCommand(command, { stateRoot }) {
|
|
33
|
+
if (!stateRoot) return ["Error: workspace registry is not available."];
|
|
34
|
+
if (command.type === "add") {
|
|
35
|
+
const rootPath = resolve(command.path);
|
|
36
|
+
if (!existsSync(rootPath)) return [`Error: project path does not exist: ${rootPath}`];
|
|
37
|
+
const project = registerProject({ stateRoot, rootPath });
|
|
38
|
+
return [`Registered project: ${project.displayName}`, brightBlack(project.rootPath)];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const projects = listRegisteredProjects({ stateRoot });
|
|
42
|
+
if (projects.length === 0) return ["No registered projects."];
|
|
43
|
+
return ["Registered projects:", ...projects.map((project) => `- ${project.displayName} ${brightBlack(project.rootPath)}`)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handleSessionCommand({ stateRoot, currentProjectId, runner, workspaceSupervisor, workspaceOutputRouter, ui }) {
|
|
47
|
+
if (!stateRoot) {
|
|
48
|
+
ui.writeln("Session selector is not available: workspace registry is missing.");
|
|
49
|
+
return { handled: true };
|
|
50
|
+
}
|
|
51
|
+
const projects = await listWorkspaceSessions({ stateRoot, currentProjectId });
|
|
52
|
+
const currentSessionId = runner.getSessionStats?.().sessionId ?? null;
|
|
53
|
+
const runtimeSummaries = workspaceSupervisor?.getRuntimeSummaries?.() ?? [];
|
|
54
|
+
const items = annotateWorkspaceItems(buildWorkspaceSessionSelectItems(projects, currentSessionId), runtimeSummaries);
|
|
55
|
+
if (items.length === 0) {
|
|
56
|
+
ui.writeln("No registered projects. Start March in a project or run /project add <path>.");
|
|
57
|
+
return { handled: true };
|
|
58
|
+
}
|
|
59
|
+
if (!ui.selectList) {
|
|
60
|
+
ui.writeln("Session selector is only available in TUI.");
|
|
61
|
+
return { handled: true };
|
|
62
|
+
}
|
|
63
|
+
const selectedIndex = Math.max(0, items.findIndex((item) => item.project.current && item.session?.id === currentSessionId));
|
|
64
|
+
const item = await ui.selectList({
|
|
65
|
+
items,
|
|
66
|
+
selectedIndex,
|
|
67
|
+
width: 90,
|
|
68
|
+
suppressInitialConfirm: true,
|
|
69
|
+
searchable: true,
|
|
70
|
+
getSearchText: workspaceSessionSearchText,
|
|
71
|
+
});
|
|
72
|
+
if (!item) {
|
|
73
|
+
ui.writeln("Session unchanged.");
|
|
74
|
+
return { handled: true };
|
|
75
|
+
}
|
|
76
|
+
if (!item.session) {
|
|
77
|
+
if (!workspaceSupervisor?.startNewWorkspaceSession) {
|
|
78
|
+
ui.writeln("New session creation requires the workspace supervisor.");
|
|
79
|
+
return { handled: true };
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const { result } = await workspaceSupervisor.startNewWorkspaceSession(item.project);
|
|
83
|
+
ui.writeln(`Created session: ${item.project.displayName} / ${result?.sessionId ?? "new session"}`);
|
|
84
|
+
return { handled: true, refreshContextTokens: true, activeChanged: true };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
ui.writeln(`Error: ${err.message}`);
|
|
87
|
+
return { handled: true };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (workspaceSupervisor) {
|
|
91
|
+
try {
|
|
92
|
+
await workspaceSupervisor.activateWorkspaceSession({ project: item.project, session: item.session });
|
|
93
|
+
ctxRenderActiveSession({ workspaceOutputRouter, projectId: item.project.projectId, sessionId: item.session.id });
|
|
94
|
+
return { handled: true, refreshContextTokens: true, activeChanged: true };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
ui.writeln(`Error: ${err.message}`);
|
|
97
|
+
return { handled: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ui.writeln("Workspace session activation requires the workspace supervisor.");
|
|
101
|
+
return { handled: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function annotateWorkspaceItems(items, runtimeSummaries) {
|
|
105
|
+
if (!runtimeSummaries.length) return items;
|
|
106
|
+
const running = new Set(runtimeSummaries.filter((runtime) => runtime.running).map((runtime) => `${runtime.projectId}:${runtime.sessionId}`));
|
|
107
|
+
return items.map((item) => {
|
|
108
|
+
if (!item.session) return item;
|
|
109
|
+
if (!running.has(`${item.project.projectId}:${item.session.id}`)) return item;
|
|
110
|
+
return { ...item, description: `running · ${item.description}` };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ctxRenderActiveSession({ workspaceOutputRouter, projectId, sessionId }) {
|
|
115
|
+
return workspaceOutputRouter?.getRenderEventCount?.(projectId, sessionId) ?? 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeLines(ui, lines) {
|
|
119
|
+
for (const line of lines) ui.writeln(line);
|
|
120
|
+
return { handled: true };
|
|
121
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const RENDER_METHODS = new Set([
|
|
2
|
+
"turnStart",
|
|
3
|
+
"turnEnd",
|
|
4
|
+
"assistantReplyEnd",
|
|
5
|
+
"textDelta",
|
|
6
|
+
"thinkingStart",
|
|
7
|
+
"thinkingDelta",
|
|
8
|
+
"thinkingEnd",
|
|
9
|
+
"thinkingBlock",
|
|
10
|
+
"toolStart",
|
|
11
|
+
"toolEnd",
|
|
12
|
+
"retryStart",
|
|
13
|
+
"retryEnd",
|
|
14
|
+
"status",
|
|
15
|
+
"recall",
|
|
16
|
+
"editDiff",
|
|
17
|
+
"write",
|
|
18
|
+
"writeln",
|
|
19
|
+
"clearOutput",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const MAX_RENDER_EVENTS_PER_ROUTE = 4000;
|
|
23
|
+
|
|
24
|
+
export function createWorkspaceOutputRouter({ ui, activeProjectId, activeSessionId = null, onRenderTimelineChange = null }) {
|
|
25
|
+
let active = routeKey(activeProjectId, activeSessionId);
|
|
26
|
+
const timelines = new Map();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
setActiveProject(projectId) {
|
|
30
|
+
this.setActiveSession(projectId, null);
|
|
31
|
+
},
|
|
32
|
+
setActiveSession(projectId, sessionId, { renderTimeline = null } = {}) {
|
|
33
|
+
const next = routeKey(projectId, sessionId);
|
|
34
|
+
if (Array.isArray(renderTimeline)) setRenderEvents(next, renderTimeline);
|
|
35
|
+
if (next === active) return renderRoute(next);
|
|
36
|
+
active = next;
|
|
37
|
+
return renderRoute(next);
|
|
38
|
+
},
|
|
39
|
+
getActiveRouteKey() {
|
|
40
|
+
return active;
|
|
41
|
+
},
|
|
42
|
+
getActiveProject() {
|
|
43
|
+
return parseRouteKey(active).projectId;
|
|
44
|
+
},
|
|
45
|
+
createProjectUi(projectId, getSessionId = null) {
|
|
46
|
+
return this.createSessionUi({ projectId, getSessionId });
|
|
47
|
+
},
|
|
48
|
+
createSessionUi({ projectId, sessionId = null, getSessionId = null }) {
|
|
49
|
+
return new Proxy({}, {
|
|
50
|
+
get(_target, prop) {
|
|
51
|
+
if (prop === "__projectId") return projectId;
|
|
52
|
+
const value = ui[prop];
|
|
53
|
+
if (typeof value !== "function") return value;
|
|
54
|
+
return (...args) => {
|
|
55
|
+
const key = routeKey(projectId, typeof getSessionId === "function" ? getSessionId() : sessionId);
|
|
56
|
+
if (!RENDER_METHODS.has(prop)) return value.apply(ui, args);
|
|
57
|
+
recordRenderEvent(key, prop, args);
|
|
58
|
+
if (key === active) return value.apply(ui, args);
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
set(_target, prop, value) {
|
|
63
|
+
ui[prop] = value;
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
has(_target, prop) {
|
|
67
|
+
return prop in ui;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
renderActiveSession() {
|
|
72
|
+
return renderRoute(active);
|
|
73
|
+
},
|
|
74
|
+
getRenderEvents(projectId, sessionId = null) {
|
|
75
|
+
return [...(timelines.get(routeKey(projectId, sessionId)) ?? [])];
|
|
76
|
+
},
|
|
77
|
+
setRenderEvents(projectId, sessionId = null, events = []) {
|
|
78
|
+
setRenderEvents(routeKey(projectId, sessionId), events);
|
|
79
|
+
},
|
|
80
|
+
getRenderEventCount(projectId, sessionId = null) {
|
|
81
|
+
return timelines.get(routeKey(projectId, sessionId))?.length ?? 0;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function renderRoute(key) {
|
|
86
|
+
ui.clearOutput?.();
|
|
87
|
+
const events = timelines.get(key) ?? [];
|
|
88
|
+
for (const event of events) applyRenderEvent(event);
|
|
89
|
+
return events.length;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyRenderEvent({ method, args }) {
|
|
93
|
+
const value = ui[method];
|
|
94
|
+
if (typeof value === "function") value.apply(ui, args);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function recordRenderEvent(key, method, args) {
|
|
98
|
+
if (method === "clearOutput") {
|
|
99
|
+
timelines.delete(key);
|
|
100
|
+
onRenderTimelineChange?.({ ...parseRouteKey(key), events: [], event: { method, args } });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const events = timelines.get(key) ?? [];
|
|
104
|
+
events.push({ method, args, at: Date.now() });
|
|
105
|
+
if (events.length > MAX_RENDER_EVENTS_PER_ROUTE) events.splice(0, events.length - MAX_RENDER_EVENTS_PER_ROUTE);
|
|
106
|
+
timelines.set(key, events);
|
|
107
|
+
onRenderTimelineChange?.({ ...parseRouteKey(key), events: [...events], event: { method, args } });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setRenderEvents(key, events) {
|
|
111
|
+
timelines.set(key, normalizeRenderEvents(events));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function routeKey(projectId, sessionId = null) {
|
|
116
|
+
return `${projectId ?? ""}:${sessionId ?? ""}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseRouteKey(key) {
|
|
120
|
+
const [projectId, sessionId = ""] = String(key ?? "").split(":", 2);
|
|
121
|
+
return { projectId: projectId || null, sessionId: sessionId || null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeRenderEvents(events) {
|
|
125
|
+
if (!Array.isArray(events)) return [];
|
|
126
|
+
return events.filter((event) => typeof event?.method === "string" && Array.isArray(event.args));
|
|
127
|
+
}
|