march-cli 0.1.37 → 0.1.38

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/agent/runner/runner-utils.mjs +20 -0
  3. package/src/agent/runner.mjs +9 -10
  4. package/src/agent/runtime/remote-ui-client.mjs +0 -1
  5. package/src/agent/runtime/runner-process-client.mjs +2 -0
  6. package/src/agent/runtime/runner-process-factory.mjs +2 -3
  7. package/src/agent/runtime/runner-runtime-host.mjs +0 -2
  8. package/src/agent/runtime/ui-event-bridge.mjs +0 -2
  9. package/src/agent/session/session-options.mjs +1 -2
  10. package/src/agent/tools.mjs +2 -23
  11. package/src/cli/args.mjs +0 -3
  12. package/src/cli/commands/registry/slash-command-registry.mjs +2 -0
  13. package/src/cli/fallback-ui.mjs +0 -2
  14. package/src/cli/input/mode-state.mjs +1 -1
  15. package/src/cli/repl-loop.mjs +67 -19
  16. package/src/cli/startup/app-runtime.mjs +69 -4
  17. package/src/cli/startup/create-runtime-runner.mjs +2 -1
  18. package/src/cli/startup/startup-session.mjs +3 -13
  19. package/src/cli/ui.mjs +0 -6
  20. package/src/cli/workspace/command.mjs +147 -0
  21. package/src/cli/workspace/output-router.mjs +108 -0
  22. package/src/cli/workspace/project-runtime.mjs +92 -0
  23. package/src/config/features.mjs +0 -1
  24. package/src/extensions/lifecycle-adapter.mjs +1 -1
  25. package/src/main.mjs +4 -0
  26. package/src/notification/desktop-notifier.mjs +16 -8
  27. package/src/web-ui/dist/assets/index-BQtl1uQs.css +1 -0
  28. package/src/web-ui/dist/assets/index-DrlJis_D.js +1845 -0
  29. package/src/web-ui/dist/index.html +13 -0
  30. package/src/web-ui/runtime-host.mjs +1 -2
  31. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +2 -10
  32. package/src/web-ui/src/mockData.ts +1 -8
  33. package/src/web-ui/src/model.ts +0 -2
  34. package/src/web-ui/src/runtime/client.ts +0 -1
  35. package/src/web-ui/src/runtime/runtimeTimeline.ts +1 -3
  36. package/src/web-ui/src/styles/shell.css +1 -2
  37. package/src/web-ui/src/timelineAdapter.ts +1 -2
  38. package/src/workspace/project-id.mjs +14 -0
  39. package/src/workspace/project-registry.mjs +74 -0
  40. package/src/workspace/session-index.mjs +75 -0
  41. package/src/workspace/supervisor.mjs +172 -0
  42. package/src/cli/permissions.mjs +0 -103
  43. package/src/cli/tui/permission-request-ui.mjs +0 -18
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,147 @@
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
+ import { resumePiSessionById } from "../session/pi-session-switch-command.mjs";
7
+ import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
8
+
9
+ export const WORKSPACE_SLASH_COMMANDS = [
10
+ {
11
+ metadata: [
12
+ { name: "project", description: "List registered projects" },
13
+ { name: "project add", helpSyntax: "project add <path>", description: "Register a project root" },
14
+ ],
15
+ match: (trimmed) => {
16
+ const parsed = parseProjectCommand(trimmed);
17
+ return parsed.type === "none" ? null : { parsed };
18
+ },
19
+ run: async (ctx, command) => writeLines(ctx.ui, await handleProjectCommand(command, ctx)),
20
+ },
21
+ {
22
+ metadata: [{ name: "switch", description: "Open cross-project session switcher" }],
23
+ match: (trimmed) => trimmed === "/switch" ? { parsed: { type: "switch" } } : null,
24
+ run: handleSwitchCommand,
25
+ },
26
+ ];
27
+
28
+ export function parseProjectCommand(trimmed) {
29
+ if (trimmed === "/project" || trimmed === "/project list") return { type: "list" };
30
+ if (trimmed.startsWith("/project add ")) return { type: "add", path: trimmed.slice("/project add ".length).trim() };
31
+ return { type: "none" };
32
+ }
33
+
34
+ export async function handleProjectCommand(command, { stateRoot }) {
35
+ if (!stateRoot) return ["Error: workspace registry is not available."];
36
+ if (command.type === "add") {
37
+ const rootPath = resolve(command.path);
38
+ if (!existsSync(rootPath)) return [`Error: project path does not exist: ${rootPath}`];
39
+ const project = registerProject({ stateRoot, rootPath });
40
+ return [`Registered project: ${project.displayName}`, brightBlack(project.rootPath)];
41
+ }
42
+
43
+ const projects = listRegisteredProjects({ stateRoot });
44
+ if (projects.length === 0) return ["No registered projects."];
45
+ return ["Registered projects:", ...projects.map((project) => `- ${project.displayName} ${brightBlack(project.rootPath)}`)];
46
+ }
47
+
48
+ export async function handleSwitchCommand({ stateRoot, currentProjectId, projectMarchDir, runner, workspaceSupervisor, workspaceOutputRouter, ui }) {
49
+ if (!stateRoot) {
50
+ ui.writeln("Session switcher is not available: workspace registry is missing.");
51
+ return { handled: true };
52
+ }
53
+ const projects = await listWorkspaceSessions({ stateRoot, currentProjectId });
54
+ const currentSessionId = runner.getSessionStats?.().sessionId ?? null;
55
+ const runtimeSummaries = workspaceSupervisor?.getRuntimeSummaries?.() ?? [];
56
+ const items = annotateWorkspaceItems(buildWorkspaceSessionSelectItems(projects, currentSessionId), runtimeSummaries);
57
+ if (items.length === 0) {
58
+ ui.writeln("No registered projects. Start March in a project or run /project add <path>.");
59
+ return { handled: true };
60
+ }
61
+ if (!ui.selectList) {
62
+ ui.writeln("Session switcher is only available in TUI.");
63
+ return { handled: true };
64
+ }
65
+ const selectedIndex = Math.max(0, items.findIndex((item) => item.project.current && item.session?.id === currentSessionId));
66
+ const item = await ui.selectList({
67
+ items,
68
+ selectedIndex,
69
+ width: 90,
70
+ suppressInitialConfirm: true,
71
+ searchable: true,
72
+ getSearchText: workspaceSessionSearchText,
73
+ });
74
+ if (!item) {
75
+ ui.writeln("Session unchanged.");
76
+ return { handled: true };
77
+ }
78
+ if (!item.session) {
79
+ if (!workspaceSupervisor?.startNewWorkspaceSession) {
80
+ ui.writeln("New session creation requires the workspace supervisor.");
81
+ return { handled: true };
82
+ }
83
+ try {
84
+ const { result } = await workspaceSupervisor.startNewWorkspaceSession(item.project);
85
+ ui.restoreTranscript?.([]);
86
+ ui.writeln(`Created session: ${item.project.displayName} / ${result?.sessionId ?? "new session"}`);
87
+ return { handled: true, refreshContextTokens: true, activeChanged: true };
88
+ } catch (err) {
89
+ ui.writeln(`Error: ${err.message}`);
90
+ return { handled: true };
91
+ }
92
+ }
93
+ if (workspaceSupervisor) {
94
+ try {
95
+ await workspaceSupervisor.activateWorkspaceSession({ project: item.project, session: item.session });
96
+ restoreTranscriptFromSession(item.session, ui);
97
+ const replayed = ctxReplayBufferedOutput({ workspaceOutputRouter, projectId: item.project.projectId, sessionId: item.session.id });
98
+ ui.writeln(`Switched to session: ${item.project.displayName} / ${item.session.name || item.session.id}${replayed ? ` (${replayed} buffered events replayed)` : ""}`);
99
+ return { handled: true, refreshContextTokens: true, activeChanged: true };
100
+ } catch (err) {
101
+ ui.writeln(`Error: ${err.message}`);
102
+ return { handled: true };
103
+ }
104
+ }
105
+ if (!item.project.current) {
106
+ ui.writeln(`Project switch target indexed: ${item.project.displayName}`);
107
+ ui.writeln(brightBlack("Cross-project attach requires the workspace supervisor."));
108
+ return { handled: true };
109
+ }
110
+ const sessions = projects.find((project) => project.current)?.sessions ?? [];
111
+ const lines = await resumePiSessionById(item.session.id, { runner, sessions, projectMarchDir });
112
+ if (isResumeSuccess(lines)) restoreTranscriptFromSession(item.session, ui);
113
+ for (const line of lines) ui.writeln(line);
114
+ return { handled: true, refreshContextTokens: isResumeSuccess(lines) };
115
+ }
116
+
117
+ function annotateWorkspaceItems(items, runtimeSummaries) {
118
+ if (!runtimeSummaries.length) return items;
119
+ const running = new Set(runtimeSummaries.filter((runtime) => runtime.running).map((runtime) => `${runtime.projectId}:${runtime.sessionId}`));
120
+ return items.map((item) => {
121
+ if (!item.session) return item;
122
+ if (!running.has(`${item.project.projectId}:${item.session.id}`)) return item;
123
+ return { ...item, description: `running · ${item.description}` };
124
+ });
125
+ }
126
+
127
+ function ctxReplayBufferedOutput({ workspaceOutputRouter, projectId, sessionId }) {
128
+ return workspaceOutputRouter?.replayBufferedCalls?.(projectId, sessionId) ?? 0;
129
+ }
130
+
131
+ function restoreTranscriptFromSession(session, ui) {
132
+ if (typeof ui.restoreTranscript !== "function") return;
133
+ try {
134
+ ui.restoreTranscript(loadPiSessionTranscriptTurns(session.path));
135
+ } catch (err) {
136
+ ui.writeln(`Warning: failed to restore session transcript: ${err.message}`);
137
+ }
138
+ }
139
+
140
+ function writeLines(ui, lines) {
141
+ for (const line of lines) ui.writeln(line);
142
+ return { handled: true };
143
+ }
144
+
145
+ function isResumeSuccess(lines) {
146
+ return Array.isArray(lines) && lines.some((line) => String(line).startsWith("Resumed pi session:"));
147
+ }
@@ -0,0 +1,108 @@
1
+ const BACKGROUND_METHODS_TO_BUFFER = new Set([
2
+ "turnStart",
3
+ "turnEnd",
4
+ "assistantReplyEnd",
5
+ "textDelta",
6
+ "thinkingStart",
7
+ "thinkingDelta",
8
+ "thinkingEnd",
9
+ "toolStart",
10
+ "toolEnd",
11
+ "retryStart",
12
+ "retryEnd",
13
+ "status",
14
+ "debugLines",
15
+ "recall",
16
+ "providerQuotaSnapshot",
17
+ "editDiff",
18
+ "writeln",
19
+ ]);
20
+
21
+ export function createWorkspaceOutputRouter({ ui, activeProjectId, activeSessionId = null }) {
22
+ let active = routeKey(activeProjectId, activeSessionId);
23
+ const buffers = new Map();
24
+
25
+ return {
26
+ setActiveProject(projectId) {
27
+ active = routeKey(projectId, null);
28
+ },
29
+ setActiveSession(projectId, sessionId) {
30
+ active = routeKey(projectId, sessionId);
31
+ },
32
+ getActiveRouteKey() {
33
+ return active;
34
+ },
35
+ getActiveProject() {
36
+ return parseRouteKey(active).projectId;
37
+ },
38
+ createProjectUi(projectId, getSessionId = null) {
39
+ return this.createSessionUi({ projectId, getSessionId });
40
+ },
41
+ createSessionUi({ projectId, sessionId = null, getSessionId = null }) {
42
+ return new Proxy({}, {
43
+ get(_target, prop) {
44
+ if (prop === "__projectId") return projectId;
45
+ const value = ui[prop];
46
+ if (typeof value !== "function") return value;
47
+ return (...args) => {
48
+ const key = routeKey(projectId, typeof getSessionId === "function" ? getSessionId() : sessionId);
49
+ if (isActiveRoute(key) || !BACKGROUND_METHODS_TO_BUFFER.has(prop)) return value.apply(ui, args);
50
+ bufferBackgroundCall(key, prop, args);
51
+ return undefined;
52
+ };
53
+ },
54
+ set(_target, prop, value) {
55
+ ui[prop] = value;
56
+ return true;
57
+ },
58
+ has(_target, prop) {
59
+ return prop in ui;
60
+ },
61
+ });
62
+ },
63
+ getBufferedCalls(projectId, sessionId = null) {
64
+ return [...(buffers.get(routeKey(projectId, sessionId)) ?? [])];
65
+ },
66
+ getBufferedCallCount(projectId, sessionId = null) {
67
+ return buffers.get(routeKey(projectId, sessionId))?.length ?? 0;
68
+ },
69
+ replayBufferedCalls(projectId, sessionId = null) {
70
+ const key = routeKey(projectId, sessionId);
71
+ const calls = buffers.get(key) ?? [];
72
+ buffers.delete(key);
73
+ for (const call of calls) replayBufferedCall(call);
74
+ return calls.length;
75
+ },
76
+ clearBufferedCalls(projectId, sessionId = null) {
77
+ buffers.delete(routeKey(projectId, sessionId));
78
+ },
79
+ };
80
+
81
+ function isActiveRoute(key) {
82
+ if (key === active) return true;
83
+ const current = parseRouteKey(active);
84
+ const candidate = parseRouteKey(key);
85
+ return current.sessionId == null && current.projectId === candidate.projectId;
86
+ }
87
+
88
+ function replayBufferedCall({ method, args }) {
89
+ const value = ui[method];
90
+ if (typeof value === "function") value.apply(ui, args);
91
+ }
92
+
93
+ function bufferBackgroundCall(key, method, args) {
94
+ const calls = buffers.get(key) ?? [];
95
+ calls.push({ method, args, at: Date.now() });
96
+ if (calls.length > 2000) calls.splice(0, calls.length - 2000);
97
+ buffers.set(key, calls);
98
+ }
99
+ }
100
+
101
+ export function routeKey(projectId, sessionId = null) {
102
+ return `${projectId ?? ""}:${sessionId ?? ""}`;
103
+ }
104
+
105
+ function parseRouteKey(key) {
106
+ const [projectId, sessionId = ""] = String(key ?? "").split(":", 2);
107
+ return { projectId: projectId || null, sessionId: sessionId || null };
108
+ }
@@ -0,0 +1,92 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { createRuntimeRunner } from "../startup/create-runtime-runner.mjs";
4
+ import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
5
+ import { createMarchAuthStorage } from "../../auth/storage.mjs";
6
+ import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
7
+ import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
8
+ import { loadKeybindings } from "../input/keybindings.mjs";
9
+ import { loadPromptTemplates } from "../input/prompt-templates.mjs";
10
+ import { loadOrCreateProjectId } from "../../workspace/project-id.mjs";
11
+
12
+ export async function createWorkspaceProjectRuntime({
13
+ project,
14
+ args,
15
+ config,
16
+ stateRoot,
17
+ memoryRoot,
18
+ profilePaths,
19
+ createMemoryStore,
20
+ provider,
21
+ serviceTier,
22
+ model,
23
+ remoteMemorySources,
24
+ createUi,
25
+ refreshStatusBar,
26
+ onNotificationActivation = null,
27
+ }) {
28
+ const cwd = project.rootPath;
29
+ const projectMarchDir = resolve(cwd, ".march");
30
+ if (!existsSync(projectMarchDir)) mkdirSync(projectMarchDir, { recursive: true });
31
+
32
+ const authConfig = createMarchAuthStorage({ provider: provider ?? "deepseek", providers: config.providers, cwd });
33
+ if (!authConfig.hasAuth) throw new Error(`no providers configured for project: ${cwd}`);
34
+
35
+ const namespace = loadOrCreateProjectId(projectMarchDir);
36
+ const extensionPaths = [
37
+ ...discoverProjectExtensionPaths(cwd),
38
+ ...args.extensions.map((extensionPath) => resolve(cwd, extensionPath)),
39
+ ];
40
+ const lifecycleManifests = loadProjectLifecycleHookManifests(cwd);
41
+ const keybindingConfig = loadKeybindings(cwd);
42
+ const promptTemplateConfig = loadPromptTemplates(cwd);
43
+ const projectShellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
44
+ const sessionsRoot = join(projectMarchDir, "sessions");
45
+ const sessionState = { sessionId: Date.now().toString(36), sessionDir: null };
46
+ sessionState.sessionDir = join(sessionsRoot, sessionState.sessionId);
47
+ const contextDumpRoot = resolve(projectMarchDir, "context-dumps", sessionState.sessionId);
48
+ const memoryStore = createMemoryStore();
49
+ const ui = createUi(sessionState);
50
+ const runner = await createRuntimeRunner({
51
+ runnerOptions: {
52
+ cwd,
53
+ modelId: model,
54
+ provider,
55
+ serviceTier,
56
+ providers: config.providers,
57
+ config,
58
+ stateRoot,
59
+ memoryRoot,
60
+ profilePaths,
61
+ namespace,
62
+ projectMarchDir,
63
+ extensionPaths,
64
+ shellRuntime: Boolean(projectShellRuntime),
65
+ lifecycleHooks: lifecycleManifests.hooks,
66
+ lifecycleDiagnostics: lifecycleManifests.diagnostics,
67
+ modelContextDumper: { enabled: args.dumpContext, rootDir: contextDumpRoot },
68
+ remoteMemorySources,
69
+ notificationContext: { projectId: project.projectId },
70
+ },
71
+ ui,
72
+ shellRuntime: projectShellRuntime,
73
+ refreshStatusBar,
74
+ onNotificationActivation,
75
+ });
76
+
77
+ return {
78
+ project,
79
+ cwd,
80
+ currentProject: basename(cwd),
81
+ runner,
82
+ ui,
83
+ memoryStore,
84
+ sessionState,
85
+ sessionsRoot,
86
+ projectMarchDir,
87
+ extensionPaths,
88
+ keybindingConfig,
89
+ promptTemplateConfig,
90
+ contextDumpRoot,
91
+ };
92
+ }
@@ -7,7 +7,6 @@ const DEFAULTS = Object.freeze({
7
7
  "experimental.web_search": true,
8
8
  "experimental.web_fetch": true,
9
9
  "experimental.shell": true,
10
- "experimental.permissions": true,
11
10
  "ui.markdown_rendering": false,
12
11
  "ui.tool_expand_per_card": false,
13
12
  "agent.plan_mode": false,
@@ -109,7 +109,7 @@ export function createMarchLifecycleAdapter({
109
109
  type: "info",
110
110
  message: hooks.size === 0
111
111
  ? "March lifecycle hook adapter is read-only; no March hooks are registered."
112
- : "March lifecycle hook adapter is read-only; registered hooks are permission-gated.",
112
+ : "March lifecycle hook adapter is read-only; registered hooks are policy-gated.",
113
113
  },
114
114
  ...adapterDiagnostics,
115
115
  ...getRuntimeDiagnostics(),
package/src/main.mjs CHANGED
@@ -77,6 +77,10 @@ export async function run(argv) {
77
77
  runner: app.runner,
78
78
  memoryStore: app.memoryStore,
79
79
  currentProject: app.currentProject,
80
+ currentProjectInfo: app.currentProjectInfo,
81
+ workspaceSupervisor: app.workspaceSupervisor,
82
+ workspaceOutputRouter: app.workspaceOutputRouter,
83
+ stateRoot,
80
84
  sessionState: app.sessionState,
81
85
  sessionsRoot: app.sessionsRoot,
82
86
  projectMarchDir: app.projectMarchDir,
@@ -12,6 +12,7 @@ export function createDesktopTurnNotifier({
12
12
  writeBell = () => process.stdout.write("\x07"),
13
13
  toastNotifier = nodeNotifier,
14
14
  config = {},
15
+ onActivation = null,
15
16
  } = {}) {
16
17
  const channels = resolveNotificationChannels(config);
17
18
  const minDurationMs = normalizeNonNegativeInteger(config.minDurationMs, 0);
@@ -31,7 +32,7 @@ export function createDesktopTurnNotifier({
31
32
  if (channels.desktop) {
32
33
  results.push({
33
34
  channel: "desktop",
34
- ...(await sendDesktopNotification({ platform, spawnProcess, toastNotifier, ...payload })),
35
+ ...(await sendDesktopNotification({ platform, spawnProcess, toastNotifier, onActivation, activation: normalizedEvent.activation, ...payload })),
35
36
  });
36
37
  }
37
38
  if (channels.bell) results.push({ channel: "bell", ...sendBellNotification({ writeBell }) });
@@ -52,14 +53,14 @@ export function createDesktopTurnNotifier({
52
53
  };
53
54
  }
54
55
 
55
- export async function sendDesktopNotification({ platform = process.platform, spawnProcess = spawn, toastNotifier = nodeNotifier, title, message, iconPath = DEFAULT_NOTIFICATION_ICON_PATH, sound = true }) {
56
+ export async function sendDesktopNotification({ platform = process.platform, spawnProcess = spawn, toastNotifier = nodeNotifier, title, message, iconPath = DEFAULT_NOTIFICATION_ICON_PATH, sound = true, activation = null, onActivation = null }) {
56
57
  if (platform !== "win32") return { ok: false, reason: "unsupported-platform" };
57
58
 
58
59
  const safeTitle = normalizeNotificationText(title) || "March";
59
60
  const safeMessage = normalizeNotificationText(message) || "Turn finished";
60
61
 
61
62
  try {
62
- const toastResult = await sendWindowsToastNotification({ toastNotifier, title: safeTitle, message: safeMessage, iconPath, sound });
63
+ const toastResult = await sendWindowsToastNotification({ toastNotifier, title: safeTitle, message: safeMessage, iconPath, sound, activation, onActivation });
63
64
  if (toastResult.ok) return { ok: true };
64
65
 
65
66
  const script = buildWindowsNotificationScript({ title: safeTitle, message: safeMessage, iconPath });
@@ -140,18 +141,19 @@ export function buildWindowsNotificationScript({ title, message, timeoutMs = DEF
140
141
  return buildWindowsBalloonScript({ title, message, timeoutMs, iconPath });
141
142
  }
142
143
 
143
- export function buildWindowsToastOptions({ title, message, iconPath = DEFAULT_NOTIFICATION_ICON_PATH, sound = true }) {
144
+ export function buildWindowsToastOptions({ title, message, iconPath = DEFAULT_NOTIFICATION_ICON_PATH, sound = true, activation = null }) {
144
145
  return {
145
146
  title,
146
147
  message,
147
148
  icon: iconPath,
148
149
  appID: "March",
149
150
  sound,
150
- wait: false,
151
+ wait: Boolean(activation),
152
+ ...(activation ? { activation } : {}),
151
153
  };
152
154
  }
153
155
 
154
- function sendWindowsToastNotification({ toastNotifier = nodeNotifier, title, message, iconPath, sound }) {
156
+ function sendWindowsToastNotification({ toastNotifier = nodeNotifier, title, message, iconPath, sound, activation = null, onActivation = null }) {
155
157
  return new Promise((resolve) => {
156
158
  const notify = toastNotifier?.notify;
157
159
  if (typeof notify !== "function") {
@@ -161,12 +163,13 @@ function sendWindowsToastNotification({ toastNotifier = nodeNotifier, title, mes
161
163
 
162
164
  let settled = false;
163
165
  const timeout = setTimeout(() => finish({ ok: false, reason: "toast-timeout" }), DEFAULT_BALLOON_TIMEOUT_MS + 5000);
164
- notify.call(toastNotifier, buildWindowsToastOptions({ title, message, iconPath, sound }), (err) => {
166
+ notify.call(toastNotifier, buildWindowsToastOptions({ title, message, iconPath, sound, activation }), (err, response, metadata) => {
165
167
  if (err) {
166
168
  finish({ ok: false, reason: err?.message ?? String(err) });
167
169
  return;
168
170
  }
169
- finish({ ok: true });
171
+ if (isNotificationActivation(response, metadata)) onActivation?.(activation);
172
+ finish({ ok: true, activated: isNotificationActivation(response, metadata) });
170
173
  });
171
174
 
172
175
  function finish(result) {
@@ -178,6 +181,11 @@ function sendWindowsToastNotification({ toastNotifier = nodeNotifier, title, mes
178
181
  });
179
182
  }
180
183
 
184
+ function isNotificationActivation(response, metadata) {
185
+ const text = `${response ?? ""} ${metadata?.activationType ?? ""} ${metadata?.activationKind ?? ""}`.toLowerCase();
186
+ return /activate|click|contentsclicked|buttonclicked/.test(text);
187
+ }
188
+
181
189
  function resolveNotificationChannels(config) {
182
190
  return {
183
191
  desktop: config.desktop !== false,
@@ -0,0 +1 @@
1
+ @layer theme.palette{:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--font-sans:"Segoe UI Variable Text", "Segoe UI Variable", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Arial, DengXian, "等线", "Microsoft YaHei UI", sans-serif;--font-mono:ui-monospace, "SFMono-Regular", "Cascadia Code", "JetBrains Mono", "Fira Code", Consolas, "Liberation Mono", Menlo, monospace;--font-weight-ui-normal:500;--font-weight-content-normal:600;--font-weight-ui-medium:600;--font-weight-ui-strong:700;--palette-white:#fff;--palette-slate-50:#f8fafc;--palette-slate-100:#f1f5f9;--palette-slate-200:#e2e8f0;--palette-slate-400:#94a3b8;--palette-slate-500:#64748b;--palette-slate-700:#334155;--palette-slate-900:#0f172a;--palette-slate-950:#020617;--palette-blue-400:#60a5fa;--palette-blue-600:#2563eb;--palette-green-700:#15803d;--palette-amber-600:#d97706;--palette-red-600:#dc2626}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}}@layer theme.semantic{:root{--color-bg-page:#f3f4f6;--color-bg-surface:var(--palette-white);--color-bg-sidebar:#ffffffbd;--color-bg-subtle:var(--palette-slate-50);--color-text-primary:var(--palette-slate-900);--color-text-muted:var(--palette-slate-500);--color-text-inverse:var(--palette-white);--color-border-subtle:#0f172a14;--color-border-strong:#0f172a24;--color-accent:var(--palette-blue-600);--color-accent-soft:#2563eb14;--color-success:var(--palette-green-700);--color-warning:var(--palette-amber-600);--color-danger:var(--palette-red-600);--color-hover:#0000000d;--color-shadow-drawer:#00000026;--color-scrim:#0000004d}@media (prefers-color-scheme:dark){:root{--color-bg-page:var(--palette-slate-900);--color-bg-surface:var(--palette-slate-950);--color-bg-sidebar:var(--palette-slate-900);--color-bg-subtle:var(--palette-slate-900);--color-text-primary:var(--palette-slate-50);--color-text-muted:var(--palette-slate-400);--color-border-subtle:#94a3b81f;--color-border-strong:#94a3b833;--color-accent:var(--palette-blue-400);--color-accent-soft:#60a5fa24;--color-hover:#94a3b814}}}@layer theme.component{:root{--shell-left-width:260px;--shell-right-width:280px;--shell-mobile-panel-width:75vw;--header-height:36px;--composer-min-height:46px;--radius-row:6px;--radius-control:8px;--radius-composer:12px;--space-panel:8px;--tree-indent:16px}}@layer base{*{box-sizing:border-box}html,body,#root{height:100%;margin:0;overflow:hidden}body{background:var(--color-bg-page);color:var(--color-text-primary);font-family:var(--font-sans);font-weight:var(--font-weight-ui-normal);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility}button,textarea{font:inherit}button{cursor:pointer}}@layer components{.app-shell{grid-template-columns:var(--shell-left-width) minmax(0, 1fr) var(--shell-right-width);background:var(--color-bg-page);height:100dvh;color:var(--color-text-primary);isolation:isolate;grid-template-rows:minmax(0,1fr) auto;grid-template-areas:"sidebar main right""sidebar footer right";display:grid;overflow:hidden}.panel{background:var(--color-bg-sidebar);min-height:0;overflow:hidden}.left-panel{border-right:1px solid var(--color-border-subtle);grid-area:sidebar}.right-panel{border-left:1px solid var(--color-border-subtle);flex-direction:column;grid-area:right;display:flex}.projects-header,.right-header,.main-header{height:var(--header-height);border-bottom:1px solid var(--color-border-subtle);background:var(--color-bg-sidebar);color:var(--color-text-muted);font-size:11px;font-weight:var(--font-weight-ui-medium);text-transform:uppercase;justify-content:space-between;align-items:center;padding:0 16px;display:flex}.projects-header h3{font:inherit;letter-spacing:.5px;margin:0}.menu-button{border-radius:var(--radius-control);width:28px;height:28px;color:var(--color-text-muted);background:0 0;border:0;justify-content:center;align-items:center;gap:3px;display:inline-flex}.menu-button span{background:currentColor;border-radius:999px;width:3.6px;height:3.6px}.menu-button:hover,.header-button:hover,.session-row:hover,.activity-row:hover{background:var(--color-hover)}.projects-body{height:calc(100% - var(--header-height));padding:var(--space-panel) 0;overflow:hidden}.project-tree-host{--trees-accent-override:var(--color-accent);--trees-bg-override:var(--color-bg-sidebar);--trees-bg-muted-override:var(--color-hover);--trees-border-color-override:var(--color-border-subtle);--trees-fg-override:var(--color-text-primary);--trees-fg-muted-override:var(--color-text-muted);--trees-selected-bg-override:var(--color-accent-soft);--trees-selected-fg-override:var(--color-accent);--trees-font-family-override:var(--font-sans);--trees-font-size-override:13px;--trees-border-radius-override:var(--radius-row);--trees-item-height:30px;--trees-item-padding-x-override:8px;--trees-item-margin-x-override:8px;--trees-item-row-gap-override:4px;--trees-level-gap-override:var(--tree-indent);--trees-padding-inline-override:0px;--trees-scrollbar-gutter-override:6px;height:100%}.timeline{background:var(--color-bg-surface);flex-direction:column;grid-area:main;min-width:0;min-height:0;display:flex;overflow:hidden}.main-header{background:var(--color-bg-surface)}.runtime-pill{color:var(--color-text-muted);background:var(--color-bg-subtle);border-radius:999px;padding:2px 7px;font-size:12px}.runtime-pill.connected{color:var(--color-accent);background:var(--color-accent-soft)}.timeline-scroll{flex:1;min-height:0;padding:16px 18px 24px;overflow:auto}.session-title{align-items:baseline;gap:8px;margin:0 0 14px 40px;display:flex}.session-title h1{font-size:17px;font-weight:var(--font-weight-ui-strong);letter-spacing:-.01em;margin:0}.session-title span{color:var(--color-text-muted);font-size:12px}.timeline-list{max-width:920px;margin:0 auto}.message-row{grid-template-columns:30px minmax(0,1fr);gap:10px;padding:10px 0;display:grid}.message-row+.message-row{border-top:1px solid var(--color-border-subtle)}.agent-dot{border-radius:var(--radius-control);background:var(--palette-slate-700);width:28px;height:28px;color:var(--color-text-inverse);place-items:center;font-size:12px;font-weight:800;display:grid}.agent-dot.march{background:var(--color-accent)}.message-body{min-width:0;color:var(--color-text-primary);font-size:14px;font-weight:var(--font-weight-content-normal);padding-top:3px;line-height:1.55}.message-body p{margin:0}.message-body time{color:var(--color-text-muted);margin-top:4px;font-size:11px;display:block}.timeline-aux{border:1px solid var(--color-border-subtle);border-radius:var(--radius-control);background:var(--color-bg-subtle);padding:7px 9px}.timeline-aux summary,.aux-title{align-items:center;gap:8px;min-height:20px;display:flex}.timeline-aux summary{cursor:pointer}.timeline-aux span,.timeline-aux em{color:var(--color-text-muted);font-size:12px;font-style:normal}.timeline-aux strong{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:13px;font-weight:var(--font-weight-ui-medium);overflow:hidden}.timeline-aux p{color:var(--color-text-muted);margin:7px 0 0;font-size:13px}.tool-block span,.diff-inline,.terminal-block pre{font-family:var(--font-mono)}.diff-inline{border-left:2px solid var(--color-border-strong);margin-top:7px;padding-left:10px;font-size:13px}.diff-line{padding:2px 0}.diff-line.add{color:var(--color-success)}.diff-line.remove,.error-block strong{color:var(--color-danger)}.diff-line.keep{color:var(--color-text-muted)}.terminal-block pre{color:var(--color-text-muted);white-space:pre-wrap;margin:7px 0 0;font-size:12px}.right-body{flex:1;min-height:0;padding:12px;overflow:auto}.workspace-picker{gap:7px;padding-bottom:10px;display:grid}.workspace-picker label{color:var(--color-text-muted);font-size:11px;font-weight:var(--font-weight-ui-medium);text-transform:uppercase}.workspace-input-row{grid-template-columns:minmax(0,1fr) auto;gap:4px;display:grid}.workspace-input-row input{border:1px solid var(--color-border-subtle);border-radius:var(--radius-control);background:var(--color-bg-surface);min-width:0;color:var(--color-text-primary);padding:7px 8px}.workspace-input-row button,.fs-entry-row button,.fs-row{border-radius:var(--radius-control);color:var(--color-text-muted);background:0 0;border:0}.workspace-input-row button{background:var(--color-accent-soft);color:var(--color-accent);padding:0 8px}.workspace-input-row button:disabled{opacity:.45}.workspace-path{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--color-text-muted);font-size:12px;overflow:hidden}.fs-entry-row{grid-template-columns:minmax(0,1fr) auto;gap:4px;display:grid}.fs-entry-row button,.fs-row{text-align:left;min-height:28px;padding:0 7px}.fs-entry-row button:first-child{text-overflow:ellipsis;white-space:nowrap;color:var(--color-text-primary);overflow:hidden}.provider-quota{border-top:1px solid var(--color-border-subtle);gap:9px;margin-top:12px;padding-top:12px;display:grid}.provider-quota-header{justify-content:space-between;align-items:center;gap:8px;font-size:12px;display:flex}.provider-quota-header span{color:var(--color-text-primary);font-weight:var(--font-weight-ui-medium)}.provider-quota-header time,.quota-row em{color:var(--color-text-muted);font-style:normal}.quota-row{gap:5px;font-size:12px;display:grid}.quota-row-main{justify-content:space-between;align-items:center;gap:8px;display:flex}.quota-row-main span{color:var(--color-text-primary)}.quota-row-main strong{color:var(--color-accent);font-size:12px}.quota-bar{background:var(--color-bg-surface);height:6px;box-shadow:inset 0 0 0 1px var(--color-border-subtle);border-radius:999px;overflow:hidden}.quota-bar span{border-radius:inherit;background:linear-gradient(90deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 72%, white));height:100%;display:block}.session-row,.activity-row{border-radius:var(--radius-control);width:100%;min-height:34px;color:var(--color-text-primary);text-align:left;background:0 0;border:0;justify-content:space-between;align-items:center;gap:8px;padding:7px 8px;font-size:13px;display:flex}.session-row.active{background:var(--color-accent-soft);color:var(--color-accent);font-weight:var(--font-weight-ui-medium)}.session-row time,.activity-row time{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--color-text-muted);font-size:12px;overflow:hidden}.right-divider{border-top:1px solid var(--color-border-subtle);color:var(--color-text-muted);font-size:11px;font-weight:var(--font-weight-ui-medium);text-transform:uppercase;margin:12px 0 4px;padding-top:12px}.activity-row{color:var(--color-text-muted)}.composer{background:var(--color-bg-surface);grid-area:footer;grid-template-columns:1fr;align-items:end;min-width:0;padding:0 16px 12px;display:grid}.composer-box{width:100%;min-height:var(--composer-min-height);border:1px solid var(--color-border-strong);border-radius:var(--radius-composer);background:var(--color-bg-sidebar);align-items:center;display:flex;position:relative;overflow:visible}.composer-box:focus-within{border-color:var(--color-accent);box-shadow:0 0 0 1px var(--color-accent-soft)}.composer textarea{resize:none;width:100%;max-height:160px;color:var(--color-text-primary);font-weight:var(--font-weight-content-normal);background:0 0;border:0;outline:0;padding:12px 124px 12px 12px}.composer-actions{align-items:center;gap:2px;display:flex;position:absolute;bottom:8px;right:8px}.session-ring,.chip-button,.icon-action,.send-icon,.mobile-toggle{border-radius:var(--radius-control);width:28px;height:28px;color:var(--color-text-muted);background:0 0;border:0;justify-content:center;align-items:center;display:inline-flex}.session-ring:before{content:"";border:2px solid var(--color-accent);border-radius:999px;width:14px;height:14px}.chip-button{width:auto;padding:0 8px;font-size:12px}.icon-action:hover,.chip-button:hover,.mobile-toggle:hover{background:var(--color-hover)}.send-icon{background:var(--color-accent);color:var(--color-text-inverse);font-size:16px;font-weight:700}.send-icon:disabled{opacity:.45}.mobile-toggle,.overlay{display:none}@media (width<=920px){html,body{width:100%;min-height:0;position:fixed;inset:0}.app-shell{height:100dvh;padding-top:env(safe-area-inset-top,0);flex-direction:column;display:flex}.timeline{flex:1;min-height:0}.composer{padding:0 1px max(2px, env(safe-area-inset-bottom));flex:none;grid-template-columns:30px minmax(0,1fr) 30px;gap:1px}.mobile-toggle{align-self:center;height:44px;display:inline-flex}.composer-box{border-radius:10px;min-height:44px}.composer textarea{padding-right:114px}.left-panel,.right-panel{top:env(safe-area-inset-top,0);z-index:30;width:var(--shell-mobile-panel-width);background:var(--color-bg-sidebar);box-shadow:4px 0 24px var(--color-shadow-drawer);transition:transform .22s cubic-bezier(.2,.8,.2,1);position:fixed;bottom:0;transform:translate(-100%)}.left-panel{border-top-right-radius:14px;border-bottom-right-radius:14px;left:0}.right-panel{box-shadow:-4px 0 24px var(--color-shadow-drawer);border-top-left-radius:14px;border-bottom-left-radius:14px;right:0;transform:translate(100%)}.app-shell[data-left-open=true] .left-panel,.app-shell[data-right-open=true] .right-panel{transform:translate(0)}.app-shell[data-left-open=true] .overlay,.app-shell[data-right-open=true] .overlay{z-index:20;background:var(--color-scrim);display:block;position:fixed;inset:0}}}