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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/src/agent/runner/runner-utils.mjs +20 -0
  3. package/src/agent/runner.mjs +16 -17
  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 +3 -4
  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/agent/turn/turn-runner.mjs +4 -4
  12. package/src/cli/args.mjs +3 -3
  13. package/src/cli/commands/mode-command.mjs +1 -0
  14. package/src/cli/commands/registry/slash-command-registry.mjs +4 -3
  15. package/src/cli/fallback-ui.mjs +0 -2
  16. package/src/cli/input/mode-state.mjs +1 -1
  17. package/src/cli/repl-commands.mjs +1 -1
  18. package/src/cli/repl-loop.mjs +67 -19
  19. package/src/cli/session/pi-session-switch-command.mjs +11 -11
  20. package/src/cli/session/session-list-command.mjs +1 -1
  21. package/src/cli/session/session-source-command.mjs +0 -76
  22. package/src/cli/startup/app-runtime.mjs +103 -4
  23. package/src/cli/startup/create-runtime-runner.mjs +2 -1
  24. package/src/cli/startup/startup-session.mjs +3 -13
  25. package/src/cli/ui.mjs +0 -6
  26. package/src/cli/workspace/command.mjs +121 -0
  27. package/src/cli/workspace/output-router.mjs +127 -0
  28. package/src/cli/workspace/project-runtime.mjs +94 -0
  29. package/src/cli/workspace/runtime-session-state.mjs +9 -0
  30. package/src/config/features.mjs +0 -1
  31. package/src/extensions/lifecycle-adapter.mjs +3 -3
  32. package/src/main.mjs +11 -1
  33. package/src/notification/desktop-notifier.mjs +16 -8
  34. package/src/session/sidecar-sync.mjs +3 -17
  35. package/src/session/sidecar.mjs +40 -41
  36. package/src/session/state/march-session-state.mjs +175 -0
  37. package/src/session/state/march-session-sync.mjs +20 -0
  38. package/src/session/state/march-session-ui-state.mjs +60 -0
  39. package/src/web-ui/dist/assets/index-BQtl1uQs.css +1 -0
  40. package/src/web-ui/dist/assets/index-DrlJis_D.js +1845 -0
  41. package/src/web-ui/dist/index.html +13 -0
  42. package/src/web-ui/runtime-host.mjs +1 -2
  43. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +2 -10
  44. package/src/web-ui/src/mockData.ts +1 -8
  45. package/src/web-ui/src/model.ts +0 -2
  46. package/src/web-ui/src/runtime/client.ts +0 -1
  47. package/src/web-ui/src/runtime/runtimeTimeline.ts +1 -3
  48. package/src/web-ui/src/styles/shell.css +1 -2
  49. package/src/web-ui/src/timelineAdapter.ts +1 -2
  50. package/src/workspace/project-id.mjs +14 -0
  51. package/src/workspace/project-registry.mjs +74 -0
  52. package/src/workspace/session-index.mjs +102 -0
  53. package/src/workspace/supervisor.mjs +178 -0
  54. package/src/agent/pi-session/pi-session-sidecar-failure.mjs +0 -10
  55. package/src/cli/permissions.mjs +0 -103
  56. package/src/cli/session/session-switch-command.mjs +0 -1
  57. package/src/cli/tui/permission-request-ui.mjs +0 -18
  58. package/src/session/persist.mjs +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
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
+ }
@@ -2,7 +2,7 @@ import { createAgentSession, ModelRegistry, SettingsManager } from "@earendil-wo
2
2
  import { createMarchAuthStorage } from "../auth/storage.mjs";
3
3
  import { ContextEngine } from "../context/engine.mjs";
4
4
  import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs";
5
- import { syncPiSessionSidecar } from "../session/sidecar-sync.mjs";
5
+ import { syncMarchSessionState } from "../session/state/march-session-sync.mjs";
6
6
  import { LspService } from "../lsp/service.mjs";
7
7
  import { formatLspServiceEvent } from "../lsp/status-message.mjs";
8
8
  import { estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
@@ -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, syncMarchSessionState: syncMarchSessionStateEnabled = false, syncPiSidecar = syncMarchSessionStateEnabled, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {}, notificationContext = null }) {
34
+ 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
  });
@@ -129,7 +125,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
129
125
  setModelCallKind: (kind) => { currentModelCallKind = kind; },
130
126
  logger: turnLog.logger,
131
127
  setPhase: turnLog.setPhase,
132
- syncCurrentPiSidecar,
128
+ syncCurrentMarchSessionState,
133
129
  autoNameSession,
134
130
  contextMode,
135
131
  recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
@@ -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;
@@ -208,14 +206,14 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
208
206
  const activeSession = sessionBinding.get();
209
207
  activeSession.setSessionName?.(name);
210
208
  engine.setSessionName(name);
211
- syncCurrentPiSidecar();
209
+ syncCurrentMarchSessionState();
212
210
  return engine.sessionName;
213
211
  },
214
212
  canSwitchPiSession() { return Boolean(runtimeHost); },
215
213
  async startNewSession() {
216
214
  if (!runtimeHost) throw new Error("pi runtime host is not enabled");
217
215
  nextTurnContextMode = "rebuild";
218
- syncCurrentPiSidecar();
216
+ syncCurrentMarchSessionState();
219
217
  const result = await runtimeHost.newSession();
220
218
  if (result?.cancelled) return { cancelled: true };
221
219
  engine.restoreSession({}, [], { replace: true });
@@ -257,9 +255,10 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
257
255
  ]);
258
256
  },
259
257
  };
260
- function syncCurrentPiSidecar() {
261
- return syncPiSessionSidecar({
262
- enabled: syncPiSidecar, projectMarchDir, engine,
258
+ return runner;
259
+ function syncCurrentMarchSessionState() {
260
+ return syncMarchSessionState({
261
+ enabled: syncPiSidecar || syncMarchSessionStateEnabled, projectMarchDir, engine,
263
262
  sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost),
264
263
  });
265
264
  }
@@ -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,
@@ -74,7 +72,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
74
72
  enabled: true,
75
73
  }),
76
74
  useRuntimeHost: true,
77
- syncPiSidecar: true,
75
+ syncMarchSessionState: true,
78
76
  lifecycleHooks: options.lifecycleHooks ?? [],
79
77
  lifecycleDiagnostics: options.lifecycleDiagnostics ?? [],
80
78
  authStorage: d.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
  }
@@ -14,7 +14,7 @@ export async function runRunnerTurn({
14
14
  setModelCallKind,
15
15
  logger = null,
16
16
  setPhase = null,
17
- syncCurrentPiSidecar,
17
+ syncCurrentMarchSessionState,
18
18
  autoNameSession,
19
19
  contextMode = "rebuild",
20
20
  recordHistory = null,
@@ -79,7 +79,7 @@ export async function runRunnerTurn({
79
79
  ui,
80
80
  turnState,
81
81
  midTurnRecallHints,
82
- syncCurrentPiSidecar,
82
+ syncCurrentMarchSessionState,
83
83
  autoNameSession,
84
84
  recordHistory,
85
85
  });
@@ -129,7 +129,7 @@ function logSessionEvent(logger, event) {
129
129
  });
130
130
  }
131
131
 
132
- function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession, recordHistory }) {
132
+ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
133
133
  closeAssistantReply({ ui, state: turnState });
134
134
  const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
135
135
  engine.setPendingAssistantRecallHints?.(assistantRecallHints);
@@ -145,7 +145,7 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
145
145
  recordHistory?.({ ...turn, thinking: assistantThinkingText(turnState), toolCalls: turnState.toolCalls });
146
146
 
147
147
  autoNameSession?.();
148
- syncCurrentPiSidecar();
148
+ syncCurrentMarchSessionState();
149
149
  }
150
150
 
151
151
  function flushAssistantRecall({ memoryStore, engine, turnState, currentProject }) {
package/src/cli/args.mjs CHANGED
@@ -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" },
@@ -27,6 +26,7 @@ export function parseCliArgs(argv) {
27
26
  workspace: { type: "string" },
28
27
  dev: { type: "boolean" },
29
28
  help: { type: "boolean", short: "h" },
29
+ version: { type: "boolean", short: "v" },
30
30
  },
31
31
  allowPositionals: true,
32
32
  });
@@ -47,7 +47,6 @@ export function parseCliArgs(argv) {
47
47
  piSessions: values["pi-sessions"] ?? false,
48
48
  piRuntimeHost: values["pi-runtime-host"] ?? false,
49
49
  shellRuntime: values["no-shell-runtime"] ? false : true,
50
- permissionMode: values["permission-mode"] ?? "bypassPermissions",
51
50
  host: values.host ?? null,
52
51
  port: values.port ?? null,
53
52
  apiPort: values["api-port"] ?? null,
@@ -57,6 +56,7 @@ export function parseCliArgs(argv) {
57
56
  workspace: values.workspace ?? null,
58
57
  dev: values.dev ?? false,
59
58
  help: values.help ?? false,
59
+ version: values.version ?? false,
60
60
  prompt: commandName ? "" : positionals.join(" "),
61
61
  };
62
62
  }
@@ -97,7 +97,6 @@ Options:
97
97
  --pi-runtime-host Force pi AgentSessionRuntime host path
98
98
  --shell-runtime Enable interactive PTY shell tools (default)
99
99
  --no-shell-runtime Disable interactive PTY shell tools and shell pane
100
- --permission-mode <mode> Permission mode: default, bypassPermissions, dontAsk (default: bypassPermissions)
101
100
  -e, --extension <path>
102
101
  Load a pi extension path in the default runtime host (repeatable)
103
102
  --host <host> With memory serve/web, bind host (default: 127.0.0.1)
@@ -108,5 +107,6 @@ Options:
108
107
  --name <name> With memory serve/add, remote memory source name
109
108
  --foreground With memory serve, run server in current process
110
109
  -h, --help Show this help
110
+ -v, --version Show the March CLI version
111
111
  `);
112
112
  }
@@ -15,6 +15,7 @@ export function handleModeCommand(command, { modeState } = {}) {
15
15
 
16
16
  if (command.type === "set") {
17
17
  modeState.set(command.mode);
18
+ return [];
18
19
  }
19
20
 
20
21
  return [`Mode: ${formatModeLabel(modeState.get())}`];
@@ -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",
@@ -225,10 +227,9 @@ function parsedCommand({ names, metadata, parse, run }) {
225
227
  function sessionSourceCommand() {
226
228
  return {
227
229
  metadata: [
228
- { name: "session", description: "Open previous session selector" },
229
230
  { name: "save", description: "Show auto-save status" },
230
231
  ],
231
- match: (trimmed) => (trimmed === "/session" || trimmed === "/save") ? { parsed: { trimmed } } : null,
232
+ match: (trimmed) => trimmed === "/save" ? { parsed: { trimmed } } : null,
232
233
  run: async (ctx, { trimmed }) => handleSessionSourceCommand(trimmed, ctx),
233
234
  };
234
235
  }
@@ -282,7 +283,7 @@ function writeLines(ui, lines) {
282
283
  export function formatHelpLines() {
283
284
  return [
284
285
  `Commands: ${getHelpCommandSyntaxes().join(", ")}`,
285
- "Sessions: /session opens previous sessions and restores the selected one.",
286
+ "Sessions: /session opens the workspace session selector.",
286
287
  "Shortcuts: Tab = toggle Do/Discuss, Esc = abort turn, Ctrl+C = abort turn / press twice to exit when idle, Ctrl+O = toggle tool output, Alt+S = shell pane, Alt+N = next shell, Alt+K/J = shell scroll, PageUp/PageDown = output scroll, Ctrl+G = external editor, Shift+Tab = thinking selector, Ctrl+T = thinking selector, Ctrl+L = model selector",
287
288
  ];
288
289
  }
@@ -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
 
@@ -57,7 +57,7 @@ export function formatHotkeysPanel(keybindings = DEFAULT_KEYBINDINGS, diagnostic
57
57
  ...formatKeybindingDiagnostics(diagnostics),
58
58
  "Input prefixes:",
59
59
  " / Slash command autocomplete",
60
- " /session Restore a previous session",
60
+ " /session Open workspace session selector",
61
61
  " /thinking Choose or list/set thinking level",
62
62
  " @ File path autocomplete",
63
63
  " ! cmd Run local shell command without sending to the model",
@@ -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 /session 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