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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -1,3 +1,13 @@
1
+ import { installCodexTransportCompression } from "./codex-transport-compression.mjs";
2
+ import { installCodexWebSocketEventDebug } from "./codex-websocket-event-debug.mjs";
3
+ import { installCodexLargeContextGuard } from "./codex-large-context-guard.mjs";
4
+
5
+ export function installRunnerProcessGuards() {
6
+ installCodexLargeContextGuard();
7
+ installCodexTransportCompression();
8
+ installCodexWebSocketEventDebug();
9
+ }
10
+
1
11
  export function providerContextToPayload(providerContext) {
2
12
  return {
3
13
  messages: [
@@ -22,3 +32,13 @@ export function notifyTurnEndDetached(turnNotifier, event, onResult = () => {})
22
32
  pending.then(onResult, () => {});
23
33
  return pending;
24
34
  }
35
+
36
+
37
+ export function buildNotificationActivation({ notificationContext, sessionStats }) {
38
+ if (!notificationContext?.projectId) return null;
39
+ return {
40
+ type: "workspace-session",
41
+ projectId: notificationContext.projectId,
42
+ sessionId: sessionStats?.sessionId ?? null,
43
+ };
44
+ }
@@ -11,11 +11,9 @@ import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
11
11
  import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
12
12
  import { createRuntimeUiBridge } from "./runtime/ui-event-bridge.mjs";
13
13
  import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
14
- import { notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
14
+ import { buildNotificationActivation, installRunnerProcessGuards, notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
15
15
  import { dumpCodexTransportDebug, getCodexTransportDebugSnapshot } from "./runner/codex-transport-debug.mjs";
16
- import { installCodexWebSocketEventDebug } from "./runner/codex-websocket-event-debug.mjs";
17
- import { installCodexTransportCompression } from "./runner/codex-transport-compression.mjs";
18
- import { applyCodexLargeContextGuardToPayload, installCodexLargeContextGuard } from "./runner/codex-large-context-guard.mjs";
16
+ import { applyCodexLargeContextGuardToPayload } from "./runner/codex-large-context-guard.mjs";
19
17
  import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
20
18
  import { createSessionBinding } from "./session/session-binding.mjs";
21
19
  import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
@@ -32,10 +30,8 @@ import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/ru
32
30
  export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
33
31
  export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
34
32
  export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
35
- 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, permissionController = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {} }) {
36
- installCodexLargeContextGuard();
37
- installCodexTransportCompression();
38
- installCodexWebSocketEventDebug();
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 }) {
34
+ installRunnerProcessGuards();
39
35
  if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
40
36
  const authConfig = authStorage ? { authStorage, hasAuth: true } : createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
41
37
  if (!authConfig.hasAuth) throw new Error("No providers configured. Run: march provider --config");
@@ -71,7 +67,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
71
67
  sessionManager: resolvedSessionManager, sessionBinding, engine, ui: runtimeUi,
72
68
  projectMarchDir,
73
69
  memoryTools, memoryStore, historyStore, shellRuntime, lspService, mcpTools, webTools,
74
- lifecycle, permissionController, extensionPaths, hostedTools,
70
+ lifecycle, extensionPaths, hostedTools,
75
71
  onRebind: (session) => {
76
72
  installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
77
73
  syncEngineSessionState(engine, session);
@@ -83,7 +79,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
83
79
  } else {
84
80
  const sessionOptions = resolveRunnerSessionOptions({
85
81
  cwd, stateRoot, provider, modelId, modelRegistry, engine, ui: runtimeUi,
86
- memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController,
82
+ memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle,
87
83
  authStorage: resolvedAuth, projectMarchDir,
88
84
  getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
89
85
  });
@@ -139,6 +135,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
139
135
  sessionName: engine.sessionName,
140
136
  draft: result?.draft ?? "",
141
137
  durationMs: Date.now() - turnStartedAt,
138
+ activation: buildNotificationActivation({ notificationContext, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost) }),
142
139
  }, (notificationResult) => { lastNotificationResult = notificationResult; });
143
140
  const lifecycleAction = lifecycle.takePendingAction();
144
141
  if (lifecycleAction) result.lifecycleAction = lifecycleAction;
@@ -150,6 +147,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
150
147
  sessionName: engine.sessionName,
151
148
  errorMessage: err?.message ?? String(err),
152
149
  durationMs: Date.now() - turnStartedAt,
150
+ activation: buildNotificationActivation({ notificationContext, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost) }),
153
151
  }, (notificationResult) => { lastNotificationResult = notificationResult; });
154
152
  turnLog.endError(err);
155
153
  throw err;
@@ -257,6 +255,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
257
255
  ]);
258
256
  },
259
257
  };
258
+ return runner;
260
259
  function syncCurrentPiSidecar() {
261
260
  return syncPiSessionSidecar({
262
261
  enabled: syncPiSidecar, projectMarchDir, engine,
@@ -15,6 +15,5 @@ export function createRemoteRuntimeUiClient(peer) {
15
15
  debugLines: (lines) => peer.notify("uiEvent", { type: "debug_lines", lines }),
16
16
  recall: ({ source, hints }) => peer.notify("uiEvent", { type: "recall", source, hints }),
17
17
  editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
18
- requestPermission: (request) => peer.call("uiRequest", { type: "permission_request", ...request }),
19
18
  };
20
19
  }
@@ -11,6 +11,7 @@ export async function createRunnerProcessClient({
11
11
  ui,
12
12
  onModelPayload = null,
13
13
  onLspStatusChange = null,
14
+ onNotificationActivation = null,
14
15
  entry = fileURLToPath(DEFAULT_ENTRY),
15
16
  forkImpl = fork,
16
17
  timeoutMs = 0,
@@ -51,6 +52,7 @@ export async function createRunnerProcessClient({
51
52
  await active?.runner?.refreshState?.();
52
53
  onLspStatusChange?.(event);
53
54
  },
55
+ notificationActivation: (activation) => onNotificationActivation?.(activation),
54
56
  },
55
57
  timeoutMs,
56
58
  });
@@ -8,7 +8,6 @@ import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
8
8
  import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
9
9
  import { initializeMcp } from "../../mcp/index.mjs";
10
10
  import { createWebToolsFromConfig } from "../../web/tools.mjs";
11
- import { createPermissionController } from "../../cli/permissions.mjs";
12
11
  import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
13
12
  import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
14
13
  import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
@@ -23,7 +22,6 @@ const DEFAULT_DEPS = {
23
22
  createMarkdownMemoryTools,
24
23
  initializeMcp,
25
24
  createWebToolsFromConfig,
26
- createPermissionController,
27
25
  resolvePiSessionManager,
28
26
  createModelContextDumper,
29
27
  createMarchAuthStorage,
@@ -85,11 +83,12 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
85
83
  maxTurns: options.config?.maxTurns ?? undefined,
86
84
  trimBatch: options.config?.trimBatch ?? undefined,
87
85
  hostedTools: options.config?.hostedTools,
88
- permissionController: d.createPermissionController({ mode: options.permissionMode }),
86
+ notificationContext: options.notificationContext,
89
87
  modelContextDumper: d.createModelContextDumper(options.modelContextDumper ?? { enabled: false }),
90
88
  turnNotifier: d.createDesktopTurnNotifier({
91
89
  enabled: Boolean(options.config?.notifications?.turnEnd),
92
90
  config: options.config?.notifications,
91
+ onActivation: (activation) => d.peer.notify("notificationActivation", activation),
93
92
  }),
94
93
  logger,
95
94
  onModelPayload: (event) => d.peer.notify("modelPayload", pickModelPayloadEvent(event)),
@@ -26,7 +26,6 @@ export async function createRunnerRuntimeHost({
26
26
  mcpTools = [],
27
27
  webTools = [],
28
28
  lifecycle = null,
29
- permissionController = null,
30
29
  extensionPaths = [],
31
30
  hostedTools = {},
32
31
  onRebind = null,
@@ -63,7 +62,6 @@ export async function createRunnerRuntimeHost({
63
62
  mcpTools,
64
63
  webTools,
65
64
  lifecycle,
66
- permissionController,
67
65
  authStorage,
68
66
  projectMarchDir,
69
67
  hostedTools,
@@ -53,7 +53,6 @@ export function createRuntimeUiClient(eventBus) {
53
53
  recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
54
54
  providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
55
55
  editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
56
- requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
57
56
  };
58
57
  }
59
58
 
@@ -75,7 +74,6 @@ export function dispatchRuntimeUiEvent(ui, event) {
75
74
  case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
76
75
  case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
77
76
  case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
78
- case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
79
77
  default: return undefined;
80
78
  }
81
79
  }
@@ -16,7 +16,6 @@ export function resolveRunnerSessionOptions({
16
16
  mcpTools = [],
17
17
  webTools = [],
18
18
  lifecycle = null,
19
- permissionController = null,
20
19
  authStorage = null,
21
20
  projectMarchDir = null,
22
21
  stateRoot = null,
@@ -32,7 +31,7 @@ export function resolveRunnerSessionOptions({
32
31
  ?? (provider && modelId ? getModel(provider, modelId) : null);
33
32
  if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
34
33
 
35
- const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
34
+ const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
36
35
  const customToolNames = customTools.map((tool) => tool.name);
37
36
  const tools = [
38
37
  ...customToolNames.filter((name) => name === "read"),
@@ -7,7 +7,6 @@ import { createReadImageTool } from "./file-tools/read-image-tool.mjs";
7
7
  import { createSendBinaryTool } from "./output/send-binary-tool.mjs";
8
8
  import { createScreenTool } from "./screen-tools/screen-tool.mjs";
9
9
  import { createListWindowsTool } from "./screen-tools/list-windows-tool.mjs";
10
- import { toolText } from "./tool-result.mjs";
11
10
  import { createShellTools } from "../shell/tools.mjs";
12
11
  import { initImageGen } from "../image-gen/index.mjs";
13
12
  import { createSuperGrokTool } from "../supergrok/tool.mjs";
@@ -15,7 +14,7 @@ import { createBrowserTools } from "../browser/tools/index.mjs";
15
14
  import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
16
15
  import { createHistorySearchTool } from "../history/tool.mjs";
17
16
 
18
- export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], historyStore = null, shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
17
+ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], historyStore = null, shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
19
18
  const commandExecTool = createCommandExecTool({ cwd });
20
19
  const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
21
20
  const contextStatsTool = createContextStatsTool({ engine });
@@ -47,25 +46,5 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], hist
47
46
  ...(authStorage ? [createSuperGrokTool({ authStorage, projectMarchDir })] : []),
48
47
  ...(authStorage ? initImageGen({ authStorage, projectMarchDir }) : []),
49
48
  ];
50
-
51
- if (!permissionController) return tools;
52
-
53
- return tools.map((tool) => {
54
- const execute = tool.execute;
55
- if (!execute) return tool;
56
- const wrapped = async (toolCallId, params, signal, onUpdate) => {
57
- const decision = await permissionController.requestApproval(
58
- tool.name,
59
- params,
60
- ui.requestPermission
61
- ? (ctx) => ui.requestPermission(ctx)
62
- : null,
63
- );
64
- if (decision.behavior === "deny") {
65
- return toolText(`Permission denied: ${decision.message}`, { error: true, permissionDenied: true });
66
- }
67
- return execute(toolCallId, params, signal, onUpdate);
68
- };
69
- return { ...tool, execute: wrapped };
70
- });
49
+ return tools;
71
50
  }
package/src/cli/args.mjs CHANGED
@@ -17,7 +17,6 @@ export function parseCliArgs(argv) {
17
17
  "pi-runtime-host": { type: "boolean" },
18
18
  "shell-runtime": { type: "boolean" },
19
19
  "no-shell-runtime": { type: "boolean" },
20
- "permission-mode": { type: "string" },
21
20
  host: { type: "string" },
22
21
  port: { type: "string" },
23
22
  "api-port": { type: "string" },
@@ -47,7 +46,6 @@ export function parseCliArgs(argv) {
47
46
  piSessions: values["pi-sessions"] ?? false,
48
47
  piRuntimeHost: values["pi-runtime-host"] ?? false,
49
48
  shellRuntime: values["no-shell-runtime"] ? false : true,
50
- permissionMode: values["permission-mode"] ?? "bypassPermissions",
51
49
  host: values.host ?? null,
52
50
  port: values.port ?? null,
53
51
  apiPort: values["api-port"] ?? null,
@@ -97,7 +95,6 @@ Options:
97
95
  --pi-runtime-host Force pi AgentSessionRuntime host path
98
96
  --shell-runtime Enable interactive PTY shell tools (default)
99
97
  --no-shell-runtime Disable interactive PTY shell tools and shell pane
100
- --permission-mode <mode> Permission mode: default, bypassPermissions, dontAsk (default: bypassPermissions)
101
98
  -e, --extension <path>
102
99
  Load a pi extension path in the default runtime host (repeatable)
103
100
  --host <host> With memory serve/web, bind host (default: 127.0.0.1)
@@ -12,6 +12,7 @@ import { handleSessionNameCommand, parseSessionNameCommand } from "../../session
12
12
  import { handleShellCommand, parseShellCommand } from "../../shell/shell-command.mjs";
13
13
  import { handleProviderCommand, parseProviderCommand } from "../provider-command.mjs";
14
14
  import { handleModeCommand, parseModeCommand } from "../mode-command.mjs";
15
+ import { WORKSPACE_SLASH_COMMANDS } from "../../workspace/command.mjs";
15
16
 
16
17
  export const SLASH_COMMANDS = [
17
18
  exactCommand({
@@ -94,6 +95,7 @@ export const SLASH_COMMANDS = [
94
95
  parse: parseThinkingCommand,
95
96
  run: async (ctx, command) => writeLines(ctx.ui, await handleThinkingCommand(command, { runner: ctx.runner, ui: ctx.ui })),
96
97
  }),
98
+ ...WORKSPACE_SLASH_COMMANDS,
97
99
  exactCommand({
98
100
  name: "status",
99
101
  description: "Show runtime status",
@@ -48,7 +48,6 @@ export function createJsonUI() {
48
48
  editDiff: (path, diffLines) => {
49
49
  stdout.write(JSON.stringify({ type: "edit_diff", path, diff: diffLines }) + "\n");
50
50
  },
51
- requestPermission: async () => true,
52
51
  setEscapeHandler: () => {},
53
52
  setCtrlCHandler: () => {},
54
53
  setShiftTabHandler: () => {},
@@ -137,7 +136,6 @@ export function createPlainUI() {
137
136
  else stdout.write(`${dim(` ${d.text}`)}\n`);
138
137
  }
139
138
  },
140
- requestPermission: async () => true,
141
139
  setEscapeHandler: () => {},
142
140
  setCtrlCHandler: () => {},
143
141
  setShiftTabHandler: () => {},
@@ -34,7 +34,7 @@ export function formatModeReminder(mode = MODES.DO) {
34
34
  "</mode>";
35
35
  }
36
36
  return "<mode>\n" +
37
- "You are in do mode. You may implement changes when the user asks for execution, following normal permissions and project rules.\n" +
37
+ "You are in do mode. You may implement changes when the user asks for execution, following normal project rules.\n" +
38
38
  "</mode>";
39
39
  }
40
40
 
@@ -37,6 +37,10 @@ export async function runInteractiveRepl({
37
37
  runner,
38
38
  memoryStore,
39
39
  currentProject,
40
+ currentProjectInfo = null,
41
+ workspaceSupervisor = null,
42
+ workspaceOutputRouter = null,
43
+ stateRoot = null,
40
44
  sessionState,
41
45
  sessionsRoot,
42
46
  projectMarchDir,
@@ -58,7 +62,8 @@ export async function runInteractiveRepl({
58
62
  let trimmed = line.trim();
59
63
  if (!trimmed) continue;
60
64
 
61
- const handledInline = handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand });
65
+ const active = getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig });
66
+ const handledInline = handleInlineCommand(trimmed, { cwd: active.cwd, ui, lastInlineShellCommand });
62
67
  if (handledInline.type === "handled") {
63
68
  lastInlineShellCommand = handledInline.lastInlineShellCommand;
64
69
  continue;
@@ -67,45 +72,70 @@ export async function runInteractiveRepl({
67
72
 
68
73
  const slashResult = await handleSlashCommand(trimmed, {
69
74
  ui,
70
- runner,
71
- sessionState,
72
- sessionsRoot,
73
- projectMarchDir,
75
+ runner: active.runner,
76
+ workspaceSupervisor,
77
+ workspaceOutputRouter,
78
+ sessionState: active.sessionState,
79
+ sessionsRoot: active.sessionsRoot,
80
+ projectMarchDir: active.projectMarchDir,
74
81
  sessionSource,
75
- extensionPaths,
76
- keybindings: keybindingConfig.keybindings,
77
- keybindingDiagnostics: keybindingConfig.diagnostics,
78
- promptTemplates: promptTemplateConfig.templates,
79
- promptTemplateDiagnostics: promptTemplateConfig.diagnostics,
82
+ extensionPaths: active.extensionPaths,
83
+ keybindings: active.keybindingConfig.keybindings,
84
+ keybindingDiagnostics: active.keybindingConfig.diagnostics,
85
+ promptTemplates: active.promptTemplateConfig.templates,
86
+ promptTemplateDiagnostics: active.promptTemplateConfig.diagnostics,
80
87
  modeState,
81
88
  renderStartupBanner,
82
89
  configHomeDir,
90
+ stateRoot,
91
+ currentProjectId: active.project?.projectId ?? null,
83
92
  });
84
93
  if (slashResult.exit) break;
85
94
  if (slashResult.handled) {
86
- refreshStatusBar(contextTokenRefreshOptions(slashResult, runner));
95
+ const refreshedActive = getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig });
96
+ refreshStatusBar(contextTokenRefreshOptions(slashResult, refreshedActive.runner));
87
97
  continue;
88
98
  }
89
99
 
90
- const templateResult = expandPromptTemplate(trimmed, promptTemplateConfig.templates);
100
+ const turnActive = getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig });
101
+ const templateResult = expandPromptTemplate(trimmed, turnActive.promptTemplateConfig.templates);
91
102
  if (templateResult.type === "template") {
92
103
  ui.writeln(brightBlack(`● template: ${templateResult.name}`));
93
104
  trimmed = templateResult.prompt;
94
105
  }
95
106
 
96
- await runReplTurn({
107
+ if (turnActive.turnTask) {
108
+ ui.writeln("This session is still running. Use /switch to start or inspect another session.");
109
+ continue;
110
+ }
111
+
112
+ startReplTurn({
113
+ runtime: turnActive,
97
114
  prompt: trimmed,
98
- runner,
99
- memoryStore,
100
- currentProject,
101
115
  ui,
102
116
  refreshStatusBar,
103
117
  setTurnRunning,
118
+ workspaceSupervisor,
104
119
  modeState,
105
120
  });
106
121
  }
107
122
  }
108
123
 
124
+ export function getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig }) {
125
+ return workspaceSupervisor?.getActive?.() ?? {
126
+ project: currentProjectInfo,
127
+ cwd,
128
+ runner,
129
+ memoryStore,
130
+ currentProject,
131
+ sessionState,
132
+ sessionsRoot,
133
+ projectMarchDir,
134
+ extensionPaths,
135
+ keybindingConfig,
136
+ promptTemplateConfig,
137
+ };
138
+ }
109
139
  export function contextTokenRefreshOptions(slashResult, runner) {
110
140
  if (!slashResult?.refreshContextTokens) return undefined;
111
141
  if (typeof runner.estimateContextTokens !== "function") return undefined;
@@ -127,6 +157,27 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
127
157
  return { type: "none" };
128
158
  }
129
159
 
160
+ function startReplTurn({ runtime, prompt, ui, refreshStatusBar, setTurnRunning, workspaceSupervisor = null, modeState = null }) {
161
+ const turnUi = runtime.ui ?? ui;
162
+ const task = runReplTurn({
163
+ prompt,
164
+ runner: runtime.runner,
165
+ memoryStore: runtime.memoryStore,
166
+ currentProject: runtime.currentProject,
167
+ ui: turnUi,
168
+ refreshStatusBar,
169
+ setTurnRunning,
170
+ modeState,
171
+ }).finally(() => {
172
+ if (runtime.turnTask === task) runtime.turnTask = null;
173
+ const hasRunningTurn = Boolean(workspaceSupervisor?.hasRunningTurn?.());
174
+ setTurnRunning(hasRunningTurn);
175
+ if (!hasRunningTurn) refreshStatusBar.stopWorking?.();
176
+ refreshStatusBar();
177
+ });
178
+ runtime.turnTask = task;
179
+ }
180
+
130
181
  async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
131
182
  memoryStore.beginTurn();
132
183
  try {
@@ -143,10 +194,7 @@ async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, re
143
194
  } catch (err) {
144
195
  ui.writeln(`Error: ${err.message}`);
145
196
  } finally {
146
- setTurnRunning(false);
147
- refreshStatusBar.stopWorking?.();
148
197
  memoryStore.endTurn();
149
- refreshStatusBar();
150
198
  }
151
199
  }
152
200
 
@@ -19,6 +19,11 @@ 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";
22
27
 
23
28
  export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot } = {}) {
24
29
  if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
@@ -56,6 +61,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
56
61
  const inputHistoryStore = createInputHistoryStore({ path: join(projectMarchDir, "input-history.json") });
57
62
  const modeState = createModeState();
58
63
  const namespace = loadOrCreateProjectId(projectMarchDir);
64
+ const currentProjectInfo = registerProject({ stateRoot, rootPath: cwd });
59
65
  const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
60
66
  const profilePaths = defaultProfilePaths();
61
67
  ensureProfileFiles(profilePaths);
@@ -64,7 +70,6 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
64
70
  const currentProject = basename(cwd);
65
71
  const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
66
72
 
67
- const permissionMode = args.permissionMode;
68
73
  const sessionSource = "pi";
69
74
  const sessionsRoot = join(projectMarchDir, "sessions");
70
75
  const sessionState = {
@@ -82,7 +87,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
82
87
  shellRuntime,
83
88
  historyStore: inputHistoryStore,
84
89
  });
85
-
90
+ const outputRouter = createWorkspaceOutputRouter({ ui, activeProjectId: currentProjectInfo.projectId, activeSessionId: sessionState.sessionId });
91
+ const runtimeUi = outputRouter.createProjectUi(currentProjectInfo.projectId, () => sessionState.sessionId);
86
92
  let turnRunning = false;
87
93
  let refreshStatusBar = null;
88
94
  const runnerOptions = {
@@ -98,21 +104,26 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
98
104
  namespace,
99
105
  projectMarchDir,
100
106
  extensionPaths,
101
- permissionMode,
102
107
  shellRuntime: Boolean(shellRuntime),
103
108
  lifecycleHooks: lifecycleManifests.hooks,
104
109
  lifecycleDiagnostics: lifecycleManifests.diagnostics,
105
110
  modelContextDumper: { enabled: args.dumpContext, rootDir: contextDumpRoot },
106
111
  remoteMemorySources,
112
+ notificationContext: { projectId: currentProjectInfo.projectId },
107
113
  };
108
114
 
109
115
  let runner;
116
+ let workspaceSupervisor = null;
117
+ const onNotificationActivation = (activation) => {
118
+ handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, outputRouter, ui }).catch((err) => ui.writeln(`Notification activation failed: ${err.message}`));
119
+ };
110
120
  try {
111
121
  runner = await createRuntimeRunner({
112
122
  runnerOptions,
113
- ui,
123
+ ui: runtimeUi,
114
124
  shellRuntime,
115
125
  refreshStatusBar: (...args) => refreshStatusBar?.(...args),
126
+ onNotificationActivation,
116
127
  });
117
128
  } catch (err) {
118
129
  process.stderr.write(`Error: ${err.message}\n`);
@@ -121,6 +132,42 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
121
132
  return { ok: false, code: 1, logger };
122
133
  }
123
134
 
135
+ const initialRuntime = {
136
+ project: currentProjectInfo,
137
+ cwd,
138
+ currentProject,
139
+ runner,
140
+ ui: runtimeUi,
141
+ memoryStore,
142
+ sessionState,
143
+ sessionsRoot,
144
+ projectMarchDir,
145
+ extensionPaths,
146
+ keybindingConfig,
147
+ promptTemplateConfig,
148
+ };
149
+ workspaceSupervisor = createWorkspaceSessionSupervisor({
150
+ 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),
168
+ });
169
+ runner = workspaceSupervisor.runner;
170
+
124
171
  refreshStatusBar = createStatusLineUpdater({
125
172
  ui,
126
173
  runner,
@@ -150,6 +197,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
150
197
  projectMarchDir,
151
198
  ui,
152
199
  });
200
+ workspaceSupervisor.refreshActiveRuntime();
201
+ outputRouter.setActiveSession(currentProjectInfo.projectId, sessionState.sessionId);
153
202
  refreshStatusBar();
154
203
 
155
204
  return {
@@ -158,8 +207,11 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
158
207
  cwd,
159
208
  ui,
160
209
  runner,
210
+ workspaceSupervisor,
211
+ workspaceOutputRouter: outputRouter,
161
212
  memoryStore,
162
213
  currentProject,
214
+ currentProjectInfo,
163
215
  sessionState,
164
216
  sessionsRoot,
165
217
  projectMarchDir,
@@ -175,3 +227,16 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
175
227
  setTurnRunning(value) { turnRunning = value; },
176
228
  };
177
229
  }
230
+
231
+ async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, outputRouter, ui }) {
232
+ if (activation?.type !== "workspace-session" || !activation.projectId) return;
233
+ if (!workspaceSupervisor) throw new Error("workspace supervisor is not ready");
234
+ const projects = await listWorkspaceSessions({ stateRoot, currentProjectId: workspaceSupervisor.getActive?.()?.project?.projectId ?? null });
235
+ const runtime = await workspaceSupervisor.activateWorkspaceSessionById({
236
+ projects,
237
+ projectId: activation.projectId,
238
+ sessionId: activation.sessionId,
239
+ });
240
+ outputRouter?.replayBufferedCalls?.(activation.projectId, activation.sessionId);
241
+ ui.writeln(`Activated session from notification: ${runtime.project.displayName}`);
242
+ }
@@ -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 { randomUUID } from "node:crypto";
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 function loadOrCreateProjectId(projectMarchDir) {
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,