march-cli 0.1.33 → 0.1.35

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 (55) hide show
  1. package/package.json +12 -1
  2. package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
  3. package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
  4. package/src/agent/runner.mjs +9 -14
  5. package/src/agent/runtime/remote-runner-client.mjs +7 -15
  6. package/src/agent/runtime/runner-ipc-target.mjs +3 -22
  7. package/src/agent/runtime/runner-process-client.mjs +101 -24
  8. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  9. package/src/agent/runtime/state/runner-state.mjs +80 -0
  10. package/src/agent/session/session-options.mjs +2 -1
  11. package/src/agent/tools.mjs +3 -1
  12. package/src/cli/args.mjs +14 -3
  13. package/src/cli/commands/catalog/visible-commands.mjs +5 -0
  14. package/src/cli/commands/help-command.mjs +1 -7
  15. package/src/cli/commands/registry/slash-command-registry.mjs +293 -0
  16. package/src/cli/input/autocomplete.mjs +2 -25
  17. package/src/cli/repl-loop.mjs +24 -41
  18. package/src/cli/slash-commands.mjs +19 -185
  19. package/src/cli/startup/app-runtime.mjs +201 -0
  20. package/src/cli/startup/configured-command.mjs +9 -0
  21. package/src/cli/startup/early-command.mjs +29 -0
  22. package/src/cli/turn/turn-input-preparer.mjs +41 -0
  23. package/src/main.mjs +47 -242
  24. package/src/memory/markdown/memory-id.mjs +36 -0
  25. package/src/memory/markdown-store.mjs +17 -6
  26. package/src/memory/markdown-tools.mjs +3 -2
  27. package/src/web-ui/command.mjs +112 -0
  28. package/src/web-ui/dist/assets/index-BUmhnID4.css +1 -0
  29. package/src/web-ui/dist/assets/index-CtuqTjcB.js +1845 -0
  30. package/src/web-ui/dist/index.html +13 -0
  31. package/src/web-ui/index.html +12 -0
  32. package/src/web-ui/runtime-host.mjs +185 -0
  33. package/src/web-ui/server.mjs +139 -0
  34. package/src/web-ui/session-manager.mjs +109 -0
  35. package/src/web-ui/src/App.tsx +7 -0
  36. package/src/web-ui/src/components/AppShell.tsx +47 -0
  37. package/src/web-ui/src/components/Composer.tsx +47 -0
  38. package/src/web-ui/src/components/FileExplorer.tsx +46 -0
  39. package/src/web-ui/src/components/RightSidebar.tsx +70 -0
  40. package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
  41. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
  42. package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
  43. package/src/web-ui/src/fileTreeAdapter.ts +51 -0
  44. package/src/web-ui/src/main.tsx +11 -0
  45. package/src/web-ui/src/mockData.ts +87 -0
  46. package/src/web-ui/src/model.ts +62 -0
  47. package/src/web-ui/src/runtime/client.ts +74 -0
  48. package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
  49. package/src/web-ui/src/runtime/useWebRuntime.ts +132 -0
  50. package/src/web-ui/src/styles/shell.css +156 -0
  51. package/src/web-ui/src/styles/tokens.css +116 -0
  52. package/src/web-ui/src/timelineAdapter.ts +43 -0
  53. package/src/web-ui/src/vite-env.d.ts +1 -0
  54. package/src/web-ui/tsconfig.json +20 -0
  55. package/src/web-ui/vite.config.mjs +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -22,6 +22,9 @@
22
22
  "docs:dev": "vitepress dev docs --host 127.0.0.1",
23
23
  "docs:build": "vitepress build docs",
24
24
  "docs:preview": "vitepress preview docs --host 127.0.0.1",
25
+ "web:dev": "node bin/march.mjs web --dev",
26
+ "web:build": "tsc -p src/web-ui/tsconfig.json && vite build --config src/web-ui/vite.config.mjs",
27
+ "web:preview": "npm run web:build && node src/web-ui/server.mjs",
25
28
  "context": "cd .. && node march-cli/bin/march.mjs --dump-context",
26
29
  "notify:experiment": "node scripts/notify-experiment.mjs",
27
30
  "publish:env": "node scripts/npm-publish-from-env.mjs"
@@ -31,10 +34,13 @@
31
34
  "@earendil-works/pi-coding-agent": "^0.74.0",
32
35
  "@earendil-works/pi-tui": "^0.74.0",
33
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
+ "@pierre/trees": "^1.0.0-beta.4",
34
38
  "@xterm/headless": "^5.5.0",
35
39
  "marked": "^18.0.3",
36
40
  "node-notifier": "^10.0.1",
37
41
  "node-pty": "^1.1.0",
42
+ "react": "^18.3.1",
43
+ "react-dom": "^18.3.1",
38
44
  "typebox": "^1.0.58",
39
45
  "undici": "^7.25.0",
40
46
  "web-tree-sitter": "^0.26.8",
@@ -44,6 +50,11 @@
44
50
  "@vscode/ripgrep": "^1.18.0"
45
51
  },
46
52
  "devDependencies": {
53
+ "@types/react": "^18.3.29",
54
+ "@types/react-dom": "^18.3.7",
55
+ "@vitejs/plugin-react": "^6.0.2",
56
+ "typescript": "^6.0.3",
57
+ "vite": "^8.0.14",
47
58
  "vitepress": "^1.6.4"
48
59
  }
49
60
  }
@@ -0,0 +1,16 @@
1
+ export function createRunnerLifecycle() {
2
+ let pendingAction = null;
3
+ return {
4
+ requestRuntimeRestart({ reason = "" } = {}) {
5
+ pendingAction = { type: "restart_runtime", reason };
6
+ },
7
+ takePendingAction() {
8
+ const action = pendingAction;
9
+ pendingAction = null;
10
+ return action;
11
+ },
12
+ clearPendingAction() {
13
+ pendingAction = null;
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,22 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { toolText } from "../tool-result.mjs";
4
+
5
+ export function createRuntimeRestartTool({ lifecycle }) {
6
+ return defineTool({
7
+ name: "request_runtime_restart",
8
+ label: "Request Runtime Restart",
9
+ description: "Request March to restart the runtime after the current turn so the next turn loads updated runner/tool code from disk.",
10
+ parameters: Type.Object({
11
+ reason: Type.Optional(Type.String({ description: "Why the runtime needs to restart" })),
12
+ }),
13
+ execute: async (_toolCallId, params = {}) => {
14
+ const reason = String(params.reason ?? "").trim();
15
+ lifecycle?.requestRuntimeRestart?.({ reason });
16
+ return toolText(
17
+ "March runtime restart requested. The current turn will finish first; the next turn will use the latest code from disk.",
18
+ { lifecycleAction: { type: "restart_runtime", reason } },
19
+ );
20
+ },
21
+ });
22
+ }
@@ -26,6 +26,7 @@ import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastPro
26
26
  import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
27
27
  import { registerCustomProviders } from "../provider/custom-provider.mjs";
28
28
  import { injectHostedTools } from "../provider/hosted-tools.mjs";
29
+ import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
29
30
  export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
30
31
  export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
31
32
  export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
@@ -58,6 +59,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
58
59
  const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
59
60
  const sessionBinding = createSessionBinding(null);
60
61
  let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
62
+ const lifecycle = createRunnerLifecycle();
61
63
  let currentTurnContextMode = "rebuild";
62
64
  let nextTurnContextMode = "rebuild";
63
65
  let lastNotificationResult = null, runtimeHost = null, lifecycleAdapter = null;
@@ -70,7 +72,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
70
72
  sessionManager: resolvedSessionManager, sessionBinding, engine, ui: runtimeUi,
71
73
  projectMarchDir,
72
74
  memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
73
- permissionController, extensionPaths, hostedTools,
75
+ lifecycle, permissionController, extensionPaths, hostedTools,
74
76
  onRebind: (session) => {
75
77
  installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
76
78
  syncEngineSessionState(engine, session);
@@ -82,7 +84,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
82
84
  } else {
83
85
  const sessionOptions = resolveRunnerSessionOptions({
84
86
  cwd, stateRoot, provider, modelId, modelRegistry, engine, ui: runtimeUi,
85
- memoryTools, shellRuntime, lspService, mcpTools, webTools, permissionController,
87
+ memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController,
86
88
  authStorage: resolvedAuth, projectMarchDir,
87
89
  getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
88
90
  });
@@ -115,6 +117,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
115
117
  const contextMode = nextTurnContextMode;
116
118
  currentTurnContextMode = contextMode;
117
119
  nextTurnContextMode = "rebuild";
120
+ lifecycle.clearPendingAction();
118
121
  const turnStartedAt = Date.now();
119
122
  const codexTransportStatsBefore = getCodexTransportDebugSnapshot(sessionBinding.get());
120
123
  const turnLog = beginLoggedTurn({ logger, engine, modelId, provider, contextMode, userMessage, userRecallHints, startedAt: turnStartedAt }); currentTurnId = turnLog.turnId;
@@ -135,6 +138,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
135
138
  draft: result?.draft ?? "",
136
139
  durationMs: Date.now() - turnStartedAt,
137
140
  }, (notificationResult) => { lastNotificationResult = notificationResult; });
141
+ const lifecycleAction = lifecycle.takePendingAction();
142
+ if (lifecycleAction) result.lifecycleAction = lifecycleAction;
138
143
  turnLog.endSuccess(result);
139
144
  return result;
140
145
  } catch (err) {
@@ -186,21 +191,11 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
186
191
  return model;
187
192
  },
188
193
  getScopedModels() { return appendFastVariants(sessionBinding.get().scopedModels); },
189
- getConfiguredProviders() {
190
- const configured = Object.values(providers ?? {}).map((profile) => profile?.type).filter(Boolean);
191
- const available = (modelRegistry.getAvailable?.() ?? []).map((model) => model.provider);
192
- return [...new Set([...configured, ...available])];
193
- },
194
+ getConfiguredProviders() { return [...new Set([...Object.values(providers ?? {}).map((profile) => profile?.type).filter(Boolean), ...(modelRegistry.getAvailable?.() ?? []).map((model) => model.provider)])]; },
194
195
  getSessionStats() { return getRunnerSessionStats(sessionBinding.get(), runtimeHost); },
195
196
  getLastNotificationResult() { return lastNotificationResult; },
196
197
  async notifyTest({ title = "March", message = "If you see this, March runtime notifications work." } = {}) {
197
- lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, {
198
- status: "success",
199
- sessionName: engine.sessionName,
200
- title,
201
- message,
202
- durationMs: 0,
203
- });
198
+ lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, { status: "success", sessionName: engine.sessionName, title, message, durationMs: 0 });
204
199
  return lastNotificationResult;
205
200
  },
206
201
  estimateContextTokens(userMessage = "") {
@@ -1,8 +1,14 @@
1
+ import { createRunnerEngineStateFacade } from "./state/runner-state.mjs";
2
+
1
3
  export function createRemoteRunnerClient(peer, { initialState = null } = {}) {
2
4
  let state = initialState;
5
+ const engineFacade = createRunnerEngineStateFacade({
6
+ getState: () => state,
7
+ setState: (nextState) => { state = nextState; },
8
+ });
3
9
 
4
10
  const client = {
5
- get engine() { return createEngineFacade(); },
11
+ get engine() { return engineFacade; },
6
12
  get runtimeState() { return state; },
7
13
  async init(options = {}) {
8
14
  state = await peer.call("init", options);
@@ -56,18 +62,4 @@ export function createRemoteRunnerClient(peer, { initialState = null } = {}) {
56
62
  return response;
57
63
  }
58
64
 
59
- function createEngineFacade() {
60
- const engine = state?.engine ?? {};
61
- return {
62
- ...engine,
63
- hasRenderedPendingAssistantRecallHints: () => true,
64
- takePendingAssistantRecallHints: () => [],
65
- peekPendingAssistantRecallHints: () => [],
66
- markPendingAssistantRecallHintsRendered: () => {},
67
- getRecentRecallMemoryIds: () => [],
68
- restoreSession: () => {
69
- throw new Error("remote runner session restore is not available");
70
- },
71
- };
72
- }
73
65
  }
@@ -1,3 +1,5 @@
1
+ import { createRunnerStateSnapshot } from "./state/runner-state.mjs";
2
+
1
3
  export function createRunnerIpcTarget({ createRunnerImpl, runnerOptions = {} } = {}) {
2
4
  if (typeof createRunnerImpl !== "function") throw new Error("createRunnerImpl is required");
3
5
 
@@ -100,26 +102,5 @@ export function createRunnerIpcTarget({ createRunnerImpl, runnerOptions = {} } =
100
102
  }
101
103
 
102
104
  export function getRunnerState(runner) {
103
- const currentModel = runner.getCurrentModel?.() ?? null;
104
- const scopedModels = runner.getScopedModels?.() ?? [];
105
- const thinkingLevel = runner.getThinkingLevel?.() ?? runner.engine?.thinkingLevel ?? null;
106
- return {
107
- engine: {
108
- cwd: runner.engine?.cwd ?? null,
109
- modelId: runner.engine?.modelId ?? currentModel?.id ?? null,
110
- provider: runner.engine?.provider ?? currentModel?.provider ?? null,
111
- thinkingLevel,
112
- sessionName: runner.engine?.sessionName ?? "",
113
- turns: runner.engine?.turns ?? [],
114
- },
115
- currentModel,
116
- scopedModels,
117
- configuredProviders: runner.getConfiguredProviders?.() ?? [],
118
- availableThinkingLevels: runner.getAvailableThinkingLevels?.() ?? [],
119
- canSwitchPiSession: runner.canSwitchPiSession?.() ?? false,
120
- sessionStats: runner.getSessionStats?.() ?? null,
121
- lspStatus: runner.getLspStatus?.() ?? null,
122
- extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
123
- extensionLifecycleState: runner.getExtensionLifecycleState?.() ?? null,
124
- };
105
+ return createRunnerStateSnapshot(runner);
125
106
  }
@@ -14,34 +14,111 @@ export async function createRunnerProcessClient({
14
14
  forkImpl = fork,
15
15
  timeoutMs = 0,
16
16
  } = {}) {
17
- const child = forkImpl(entry, [], {
18
- stdio: ["ignore", "inherit", "inherit", "ipc"],
19
- });
20
- const peer = createProcessRuntimeIpcPeer({
21
- processLike: child,
22
- target: {
23
- ...createRuntimeUiEventTarget(ui),
24
- modelPayload: (event) => onModelPayload?.(event),
17
+ let active = await startRuntime();
18
+ let disposed = false;
19
+ const localProps = new Map();
20
+
21
+ const runner = new Proxy({}, {
22
+ get(_target, prop) {
23
+ if (prop === "restartRuntime") return restartRuntime;
24
+ if (prop === "dispose") return dispose;
25
+ if (localProps.has(prop)) return localProps.get(prop);
26
+ const value = active.runner[prop];
27
+ return typeof value === "function" ? value.bind(active.runner) : value;
28
+ },
29
+ set(_target, prop, value) {
30
+ localProps.set(prop, value);
31
+ return true;
32
+ },
33
+ has(_target, prop) {
34
+ return prop === "restartRuntime" || prop === "dispose" || localProps.has(prop) || prop in active.runner;
25
35
  },
26
- timeoutMs,
27
36
  });
28
- const runner = createRemoteRunnerClient(peer);
29
- try {
30
- await runner.init(runnerOptions);
31
- } catch (error) {
32
- peer.dispose();
33
- child.kill?.();
34
- throw error;
35
- }
36
37
 
37
- const remoteDispose = runner.dispose;
38
- const dispose = async () => {
38
+ return { runner, get child() { return active.child; }, dispose };
39
+
40
+ async function startRuntime() {
41
+ const child = forkImpl(entry, [], {
42
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
43
+ });
44
+ const peer = createProcessRuntimeIpcPeer({
45
+ processLike: child,
46
+ target: {
47
+ ...createRuntimeUiEventTarget(ui),
48
+ modelPayload: (event) => onModelPayload?.(event),
49
+ },
50
+ timeoutMs,
51
+ });
52
+ const remoteRunner = createRemoteRunnerClient(peer);
39
53
  try {
40
- await remoteDispose.call(runner);
41
- } finally {
54
+ await waitForRuntimeInit({ child, initPromise: remoteRunner.init(runnerOptions) });
55
+ } catch (error) {
56
+ peer.dispose();
42
57
  child.kill?.();
58
+ throw error;
59
+ }
60
+ return {
61
+ runner: remoteRunner,
62
+ child,
63
+ async dispose() {
64
+ try {
65
+ await remoteRunner.dispose();
66
+ } finally {
67
+ child.kill?.();
68
+ }
69
+ },
70
+ };
71
+ }
72
+
73
+ async function restartRuntime({ restoreSession = true } = {}) {
74
+ if (disposed) throw new Error("runtime runner is already disposed");
75
+ const previousState = await refreshRunnerState(active.runner);
76
+ const previousSessionFile = previousState?.sessionStats?.sessionFile ?? previousState?.sessionStats?.sessionPath ?? null;
77
+ const previousActive = active;
78
+ await previousActive.dispose();
79
+ active = await startRuntime();
80
+
81
+ // Rebind the same persisted pi session after the fresh child imports updated source.
82
+ if (restoreSession && previousSessionFile && active.runner.canSwitchPiSession?.()) {
83
+ await active.runner.switchPiSession(previousSessionFile, previousState?.engine ?? null);
43
84
  }
44
- };
45
- runner.dispose = dispose;
46
- return { runner, child, dispose };
85
+ return await refreshRunnerState(active.runner);
86
+ }
87
+
88
+ async function dispose() {
89
+ if (disposed) return;
90
+ disposed = true;
91
+ await active.dispose();
92
+ }
47
93
  }
94
+
95
+ async function refreshRunnerState(runner) {
96
+ if (typeof runner.refreshState === "function") return await runner.refreshState();
97
+ return runner.runtimeState ?? null;
98
+ }
99
+
100
+ function waitForRuntimeInit({ child, initPromise }) {
101
+ return new Promise((resolve, reject) => {
102
+ let settled = false;
103
+ const cleanup = () => {
104
+ child.off?.("exit", onExit);
105
+ child.off?.("error", onError);
106
+ };
107
+ const finish = (fn, value) => {
108
+ if (settled) return;
109
+ settled = true;
110
+ cleanup();
111
+ fn(value);
112
+ };
113
+ const onExit = (code, signal) => {
114
+ const codeText = code == null ? "" : ` with code ${code}`;
115
+ const signalText = signal ? ` (${signal})` : "";
116
+ finish(reject, new Error(`Runtime process exited during startup${codeText}${signalText}`));
117
+ };
118
+ const onError = (error) => finish(reject, error);
119
+
120
+ child.once?.("exit", onExit);
121
+ child.once?.("error", onError);
122
+ initPromise.then((value) => finish(resolve, value), (error) => finish(reject, error));
123
+ });
124
+ }
@@ -24,6 +24,7 @@ export async function createRunnerRuntimeHost({
24
24
  lspService = null,
25
25
  mcpTools = [],
26
26
  webTools = [],
27
+ lifecycle = null,
27
28
  permissionController = null,
28
29
  extensionPaths = [],
29
30
  hostedTools = {},
@@ -59,6 +60,7 @@ export async function createRunnerRuntimeHost({
59
60
  lspService,
60
61
  mcpTools,
61
62
  webTools,
63
+ lifecycle,
62
64
  permissionController,
63
65
  authStorage,
64
66
  projectMarchDir,
@@ -0,0 +1,80 @@
1
+ export function createRunnerStateSnapshot(runner) {
2
+ const currentModel = runner.getCurrentModel?.() ?? null;
3
+ const scopedModels = runner.getScopedModels?.() ?? [];
4
+ const thinkingLevel = runner.getThinkingLevel?.() ?? runner.engine?.thinkingLevel ?? null;
5
+ const engine = runner.engine ?? {};
6
+ return {
7
+ engine: {
8
+ cwd: engine.cwd ?? null,
9
+ modelId: engine.modelId ?? currentModel?.id ?? null,
10
+ provider: engine.provider ?? currentModel?.provider ?? null,
11
+ thinkingLevel,
12
+ sessionName: engine.sessionName ?? "",
13
+ remoteMemorySources: engine.remoteMemorySources ?? [],
14
+ turns: engine.turns ?? [],
15
+ pendingAssistantRecallHints: engine.peekPendingAssistantRecallHints?.() ?? engine.pendingAssistantRecallHints ?? [],
16
+ pendingAssistantRecallHintsRendered: engine.hasRenderedPendingAssistantRecallHints?.() ?? engine.pendingAssistantRecallHintsRendered ?? false,
17
+ recentRecallMemoryIds: [...(engine.getRecentRecallMemoryIds?.() ?? [])],
18
+ },
19
+ currentModel,
20
+ scopedModels,
21
+ configuredProviders: runner.getConfiguredProviders?.() ?? [],
22
+ availableThinkingLevels: runner.getAvailableThinkingLevels?.() ?? [],
23
+ canSwitchPiSession: runner.canSwitchPiSession?.() ?? false,
24
+ sessionStats: runner.getSessionStats?.() ?? null,
25
+ lspStatus: runner.getLspStatus?.() ?? null,
26
+ extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
27
+ extensionLifecycleState: runner.getExtensionLifecycleState?.() ?? null,
28
+ };
29
+ }
30
+
31
+ export function createRunnerEngineStateFacade({ getState, setState }) {
32
+ return {
33
+ get cwd() { return engineState(getState()).cwd ?? null; },
34
+ get modelId() { return engineState(getState()).modelId ?? null; },
35
+ get provider() { return engineState(getState()).provider ?? null; },
36
+ get thinkingLevel() { return engineState(getState()).thinkingLevel ?? null; },
37
+ get sessionName() { return engineState(getState()).sessionName ?? ""; },
38
+ get remoteMemorySources() { return engineState(getState()).remoteMemorySources ?? []; },
39
+ get turns() { return engineState(getState()).turns ?? []; },
40
+ peekPendingAssistantRecallHints() {
41
+ return engineState(getState()).pendingAssistantRecallHints ?? [];
42
+ },
43
+ hasRenderedPendingAssistantRecallHints() {
44
+ return Boolean(engineState(getState()).pendingAssistantRecallHintsRendered);
45
+ },
46
+ markPendingAssistantRecallHintsRendered() {
47
+ updateEngineState(getState, setState, (engine) => {
48
+ if ((engine.pendingAssistantRecallHints ?? []).length > 0) {
49
+ engine.pendingAssistantRecallHintsRendered = true;
50
+ }
51
+ });
52
+ },
53
+ takePendingAssistantRecallHints() {
54
+ const hints = engineState(getState()).pendingAssistantRecallHints ?? [];
55
+ updateEngineState(getState, setState, (engine) => {
56
+ engine.pendingAssistantRecallHints = [];
57
+ engine.pendingAssistantRecallHintsRendered = false;
58
+ });
59
+ return hints;
60
+ },
61
+ getRecentRecallMemoryIds() {
62
+ return engineState(getState()).recentRecallMemoryIds ?? [];
63
+ },
64
+ restoreSession() {
65
+ throw new Error("remote runner session restore is not available");
66
+ },
67
+ };
68
+ }
69
+
70
+ function engineState(state) {
71
+ return state?.engine ?? {};
72
+ }
73
+
74
+ function updateEngineState(getState, setState, update) {
75
+ const state = getState();
76
+ if (!state) return;
77
+ const engine = { ...(state.engine ?? {}) };
78
+ update(engine);
79
+ setState({ ...state, engine });
80
+ }
@@ -14,6 +14,7 @@ export function resolveRunnerSessionOptions({
14
14
  lspService = null,
15
15
  mcpTools = [],
16
16
  webTools = [],
17
+ lifecycle = null,
17
18
  permissionController = null,
18
19
  authStorage = null,
19
20
  projectMarchDir = null,
@@ -30,7 +31,7 @@ export function resolveRunnerSessionOptions({
30
31
  ?? (provider && modelId ? getModel(provider, modelId) : null);
31
32
  if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
32
33
 
33
- const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
34
+ const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
34
35
  const customToolNames = customTools.map((tool) => tool.name);
35
36
  const tools = [
36
37
  ...customToolNames.filter((name) => name === "read"),
@@ -11,8 +11,9 @@ import { createShellTools } from "../shell/tools.mjs";
11
11
  import { initImageGen } from "../image-gen/index.mjs";
12
12
  import { createSuperGrokTool } from "../supergrok/tool.mjs";
13
13
  import { createBrowserTools } from "../browser/tools/index.mjs";
14
+ import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
14
15
 
15
- export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
16
+ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
16
17
  const commandExecTool = createCommandExecTool({ cwd });
17
18
  const contextStatsTool = createContextStatsTool({ engine });
18
19
  const editFileTool = createEditFileTool({ engine, ui, lspService });
@@ -35,6 +36,7 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
35
36
  ...memoryTools,
36
37
  ...mcpTools,
37
38
  ...webTools,
39
+ ...(lifecycle ? [createRuntimeRestartTool({ lifecycle })] : []),
38
40
  ...createBrowserTools({ stateRoot }),
39
41
  ...(authStorage ? [createSuperGrokTool({ authStorage, projectMarchDir })] : []),
40
42
  ...(authStorage ? initImageGen({ authStorage, projectMarchDir }) : []),
package/src/cli/args.mjs CHANGED
@@ -20,15 +20,18 @@ export function parseCliArgs(argv) {
20
20
  "permission-mode": { type: "string" },
21
21
  host: { type: "string" },
22
22
  port: { type: "string" },
23
+ "api-port": { type: "string" },
23
24
  name: { type: "string" },
24
25
  token: { type: "string" },
25
26
  foreground: { type: "boolean" },
27
+ workspace: { type: "string" },
28
+ dev: { type: "boolean" },
26
29
  help: { type: "boolean", short: "h" },
27
30
  },
28
31
  allowPositionals: true,
29
32
  });
30
33
 
31
- const commandName = ["login", "provider", "websearch", "memory", "browser", "gateway"].includes(positionals[0]) ? positionals[0] : null;
34
+ const commandName = ["login", "provider", "web", "websearch", "memory", "browser", "gateway"].includes(positionals[0]) ? positionals[0] : null;
32
35
 
33
36
  return {
34
37
  command: commandName ? { name: commandName, args: positionals.slice(1) } : null,
@@ -47,9 +50,12 @@ export function parseCliArgs(argv) {
47
50
  permissionMode: values["permission-mode"] ?? "bypassPermissions",
48
51
  host: values.host ?? null,
49
52
  port: values.port ?? null,
53
+ apiPort: values["api-port"] ?? null,
50
54
  name: values.name ?? null,
51
55
  token: values.token ?? null,
52
56
  foreground: values.foreground ?? false,
57
+ workspace: values.workspace ?? null,
58
+ dev: values.dev ?? false,
53
59
  help: values.help ?? false,
54
60
  prompt: commandName ? "" : positionals.join(" "),
55
61
  };
@@ -65,6 +71,8 @@ Usage:
65
71
  march provider --config Configure provider credentials
66
72
  march provider share [id] Share a provider profile
67
73
  march provider accept <token>
74
+ march web [path] Start the local Web UI session manager
75
+ march web --dev Start Web UI with Vite hot reload
68
76
  march websearch --config Configure web search credentials
69
77
  march memory serve [folder]
70
78
  march memory add <url>
@@ -92,8 +100,11 @@ Options:
92
100
  --permission-mode <mode> Permission mode: default, bypassPermissions, dontAsk (default: bypassPermissions)
93
101
  -e, --extension <path>
94
102
  Load a pi extension path in the default runtime host (repeatable)
95
- --host <host> With memory serve, bind host (default: 127.0.0.1)
96
- --port <port> With memory serve, bind port (default: 4317)
103
+ --host <host> With memory serve/web, bind host (default: 127.0.0.1)
104
+ --port <port> With memory serve/web, bind port
105
+ --api-port <port> With web --dev, bind API backend port
106
+ --workspace <path> With web, open an initial workspace session
107
+ --dev With web, use Vite dev server and proxy /api
97
108
  --name <name> With memory serve/add, remote memory source name
98
109
  --foreground With memory serve, run server in current process
99
110
  -h, --help Show this help
@@ -0,0 +1,5 @@
1
+ export {
2
+ getAutocompleteCommands,
3
+ getHelpCommandSyntaxes,
4
+ getVisibleCommandEntries,
5
+ } from "../registry/slash-command-registry.mjs";
@@ -1,7 +1 @@
1
- export function formatHelpLines() {
2
- return [
3
- "Commands: /new, /exit, /help, /hotkeys, /templates, /do, /discuss, /mode, /export jsonl, /export html, /export gist <jsonl|html>, /settings, /extensions, /providers, /providers <name>, /model, /models, /session, /status, /shell, /shell spawn [name], /save, /name, /copy",
4
- "Sessions: /session opens previous sessions and restores the selected one.",
5
- "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",
6
- ];
7
- }
1
+ export { formatHelpLines } from "./registry/slash-command-registry.mjs";