march-cli 0.1.34 → 0.1.36

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 (72) hide show
  1. package/package.json +12 -1
  2. package/src/agent/code-search/cache.mjs +133 -0
  3. package/src/agent/code-search/chunk-rules.mjs +107 -0
  4. package/src/agent/code-search/chunker.mjs +125 -0
  5. package/src/agent/code-search/engine.mjs +109 -0
  6. package/src/agent/code-search/languages.mjs +25 -0
  7. package/src/agent/code-search/parser-pool.mjs +29 -0
  8. package/src/agent/code-search/rerank.mjs +43 -0
  9. package/src/agent/code-search/retrieval/bm25.mjs +47 -0
  10. package/src/agent/code-search/retrieval/fusion.mjs +18 -0
  11. package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
  12. package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
  13. package/src/agent/code-search/retrieval/vector.mjs +107 -0
  14. package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
  15. package/src/agent/code-search/scanner.mjs +84 -0
  16. package/src/agent/code-search/tokenize.mjs +16 -0
  17. package/src/agent/code-search/tool.mjs +75 -0
  18. package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
  19. package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
  20. package/src/agent/runner/provider-quota-runtime.mjs +38 -0
  21. package/src/agent/runner.mjs +14 -14
  22. package/src/agent/runtime/remote-runner-client.mjs +9 -15
  23. package/src/agent/runtime/runner-ipc-target.mjs +10 -22
  24. package/src/agent/runtime/runner-process-client.mjs +101 -24
  25. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  26. package/src/agent/runtime/state/runner-state.mjs +81 -0
  27. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  28. package/src/agent/session/session-options.mjs +2 -1
  29. package/src/agent/tools.mjs +6 -1
  30. package/src/cli/args.mjs +14 -3
  31. package/src/cli/commands/catalog/visible-commands.mjs +5 -0
  32. package/src/cli/commands/help-command.mjs +1 -7
  33. package/src/cli/commands/registry/slash-command-registry.mjs +296 -0
  34. package/src/cli/commands/status-command.mjs +61 -35
  35. package/src/cli/input/autocomplete.mjs +2 -25
  36. package/src/cli/repl-loop.mjs +24 -41
  37. package/src/cli/slash-commands.mjs +19 -185
  38. package/src/cli/startup/app-runtime.mjs +201 -0
  39. package/src/cli/startup/configured-command.mjs +9 -0
  40. package/src/cli/startup/early-command.mjs +29 -0
  41. package/src/cli/turn/turn-input-preparer.mjs +41 -0
  42. package/src/context/system-core/base.md +5 -0
  43. package/src/main.mjs +47 -242
  44. package/src/provider/quota/codex.mjs +278 -0
  45. package/src/provider/quota/index.mjs +46 -0
  46. package/src/provider/quota/transport-observer.mjs +99 -0
  47. package/src/web-ui/command.mjs +112 -0
  48. package/src/web-ui/index.html +12 -0
  49. package/src/web-ui/runtime-host.mjs +188 -0
  50. package/src/web-ui/server.mjs +140 -0
  51. package/src/web-ui/session-manager.mjs +111 -0
  52. package/src/web-ui/src/App.tsx +7 -0
  53. package/src/web-ui/src/components/AppShell.tsx +48 -0
  54. package/src/web-ui/src/components/Composer.tsx +47 -0
  55. package/src/web-ui/src/components/FileExplorer.tsx +46 -0
  56. package/src/web-ui/src/components/RightSidebar.tsx +115 -0
  57. package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
  58. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
  59. package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
  60. package/src/web-ui/src/fileTreeAdapter.ts +51 -0
  61. package/src/web-ui/src/main.tsx +11 -0
  62. package/src/web-ui/src/mockData.ts +87 -0
  63. package/src/web-ui/src/model.ts +82 -0
  64. package/src/web-ui/src/runtime/client.ts +81 -0
  65. package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
  66. package/src/web-ui/src/runtime/useWebRuntime.ts +144 -0
  67. package/src/web-ui/src/styles/shell.css +166 -0
  68. package/src/web-ui/src/styles/tokens.css +116 -0
  69. package/src/web-ui/src/timelineAdapter.ts +43 -0
  70. package/src/web-ui/src/vite-env.d.ts +1 -0
  71. package/src/web-ui/tsconfig.json +20 -0
  72. package/src/web-ui/vite.config.mjs +11 -0
@@ -26,6 +26,8 @@ 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";
30
+ import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
29
31
  export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
30
32
  export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
31
33
  export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
@@ -58,6 +60,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
58
60
  const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
59
61
  const sessionBinding = createSessionBinding(null);
60
62
  let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
63
+ const lifecycle = createRunnerLifecycle();
61
64
  let currentTurnContextMode = "rebuild";
62
65
  let nextTurnContextMode = "rebuild";
63
66
  let lastNotificationResult = null, runtimeHost = null, lifecycleAdapter = null;
@@ -70,7 +73,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
70
73
  sessionManager: resolvedSessionManager, sessionBinding, engine, ui: runtimeUi,
71
74
  projectMarchDir,
72
75
  memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
73
- permissionController, extensionPaths, hostedTools,
76
+ lifecycle, permissionController, extensionPaths, hostedTools,
74
77
  onRebind: (session) => {
75
78
  installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
76
79
  syncEngineSessionState(engine, session);
@@ -82,7 +85,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
82
85
  } else {
83
86
  const sessionOptions = resolveRunnerSessionOptions({
84
87
  cwd, stateRoot, provider, modelId, modelRegistry, engine, ui: runtimeUi,
85
- memoryTools, shellRuntime, lspService, mcpTools, webTools, permissionController,
88
+ memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController,
86
89
  authStorage: resolvedAuth, projectMarchDir,
87
90
  getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
88
91
  });
@@ -105,6 +108,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
105
108
  if (serviceTier === "priority" && selectedModel && isFastProvider(selectedModel.provider)) {
106
109
  _currentFastEntry = createFastModelEntry(selectedModel).model;
107
110
  }
111
+ const providerQuotaRuntime = createRunnerProviderQuotaRuntime({ authStorage: resolvedAuth, ui: runtimeUi,
112
+ getCurrentModel: () => _currentFastEntry ?? sessionBinding.get().model });
108
113
  return {
109
114
  engine,
110
115
  get session() { return sessionBinding.get(); },
@@ -115,6 +120,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
115
120
  const contextMode = nextTurnContextMode;
116
121
  currentTurnContextMode = contextMode;
117
122
  nextTurnContextMode = "rebuild";
123
+ lifecycle.clearPendingAction();
118
124
  const turnStartedAt = Date.now();
119
125
  const codexTransportStatsBefore = getCodexTransportDebugSnapshot(sessionBinding.get());
120
126
  const turnLog = beginLoggedTurn({ logger, engine, modelId, provider, contextMode, userMessage, userRecallHints, startedAt: turnStartedAt }); currentTurnId = turnLog.turnId;
@@ -135,6 +141,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
135
141
  draft: result?.draft ?? "",
136
142
  durationMs: Date.now() - turnStartedAt,
137
143
  }, (notificationResult) => { lastNotificationResult = notificationResult; });
144
+ const lifecycleAction = lifecycle.takePendingAction();
145
+ if (lifecycleAction) result.lifecycleAction = lifecycleAction;
138
146
  turnLog.endSuccess(result);
139
147
  return result;
140
148
  } catch (err) {
@@ -171,6 +179,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
171
179
  return result;
172
180
  },
173
181
  getCurrentModel() { return _currentFastEntry ?? sessionBinding.get().model; },
182
+ ...providerQuotaRuntime,
174
183
  async setModel(model) {
175
184
  const activeSession = sessionBinding.get();
176
185
  const { baseId, isFast } = fromFastEntryModel(model);
@@ -186,21 +195,11 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
186
195
  return model;
187
196
  },
188
197
  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
- },
198
+ getConfiguredProviders() { return [...new Set([...Object.values(providers ?? {}).map((profile) => profile?.type).filter(Boolean), ...(modelRegistry.getAvailable?.() ?? []).map((model) => model.provider)])]; },
194
199
  getSessionStats() { return getRunnerSessionStats(sessionBinding.get(), runtimeHost); },
195
200
  getLastNotificationResult() { return lastNotificationResult; },
196
201
  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
- });
202
+ lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, { status: "success", sessionName: engine.sessionName, title, message, durationMs: 0 });
204
203
  return lastNotificationResult;
205
204
  },
206
205
  estimateContextTokens(userMessage = "") {
@@ -254,6 +253,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
254
253
  () => shellRuntime?.dispose?.() ?? shellRuntime?.killAll?.(),
255
254
  () => lspService.dispose(),
256
255
  () => mcpClientManager?.disconnectAll?.(),
256
+ () => providerQuotaRuntime.disposeProviderQuotaRuntime(),
257
257
  () => detachRuntimeUi(),
258
258
  ]);
259
259
  },
@@ -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);
@@ -20,6 +26,8 @@ export function createRemoteRunnerClient(peer, { initialState = null } = {}) {
20
26
  getScopedModels: () => state?.scopedModels ?? [],
21
27
  getConfiguredProviders: () => state?.configuredProviders ?? [],
22
28
  getSessionStats: () => state?.sessionStats ?? null,
29
+ getCachedProviderQuotaSnapshot: () => state?.providerQuota ?? null,
30
+ async getProviderQuotaSnapshot(options = {}) { return applyResultWithState(await peer.call("getProviderQuotaSnapshot", options)); },
23
31
  async refreshState() { return refreshState(); },
24
32
  getLastNotificationResult: () => peer.call("getLastNotificationResult"),
25
33
  notifyTest: (options) => peer.call("notifyTest", options),
@@ -56,18 +64,4 @@ export function createRemoteRunnerClient(peer, { initialState = null } = {}) {
56
64
  return response;
57
65
  }
58
66
 
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
67
  }
@@ -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
 
@@ -35,6 +37,13 @@ export function createRunnerIpcTarget({ createRunnerImpl, runnerOptions = {} } =
35
37
  getSessionStats() {
36
38
  return getRunner().getSessionStats();
37
39
  },
40
+ async getProviderQuotaSnapshot(options = {}) {
41
+ const result = await getRunner().getProviderQuotaSnapshot(options);
42
+ return { result, state: getRunnerState(runner) };
43
+ },
44
+ getCachedProviderQuotaSnapshot() {
45
+ return getRunner().getCachedProviderQuotaSnapshot?.() ?? null;
46
+ },
38
47
  getState() {
39
48
  return getRunnerState(getRunner());
40
49
  },
@@ -100,26 +109,5 @@ export function createRunnerIpcTarget({ createRunnerImpl, runnerOptions = {} } =
100
109
  }
101
110
 
102
111
  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
- };
112
+ return createRunnerStateSnapshot(runner);
125
113
  }
@@ -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,81 @@
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
+ providerQuota: runner.getCachedProviderQuotaSnapshot?.() ?? null,
26
+ lspStatus: runner.getLspStatus?.() ?? null,
27
+ extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
28
+ extensionLifecycleState: runner.getExtensionLifecycleState?.() ?? null,
29
+ };
30
+ }
31
+
32
+ export function createRunnerEngineStateFacade({ getState, setState }) {
33
+ return {
34
+ get cwd() { return engineState(getState()).cwd ?? null; },
35
+ get modelId() { return engineState(getState()).modelId ?? null; },
36
+ get provider() { return engineState(getState()).provider ?? null; },
37
+ get thinkingLevel() { return engineState(getState()).thinkingLevel ?? null; },
38
+ get sessionName() { return engineState(getState()).sessionName ?? ""; },
39
+ get remoteMemorySources() { return engineState(getState()).remoteMemorySources ?? []; },
40
+ get turns() { return engineState(getState()).turns ?? []; },
41
+ peekPendingAssistantRecallHints() {
42
+ return engineState(getState()).pendingAssistantRecallHints ?? [];
43
+ },
44
+ hasRenderedPendingAssistantRecallHints() {
45
+ return Boolean(engineState(getState()).pendingAssistantRecallHintsRendered);
46
+ },
47
+ markPendingAssistantRecallHintsRendered() {
48
+ updateEngineState(getState, setState, (engine) => {
49
+ if ((engine.pendingAssistantRecallHints ?? []).length > 0) {
50
+ engine.pendingAssistantRecallHintsRendered = true;
51
+ }
52
+ });
53
+ },
54
+ takePendingAssistantRecallHints() {
55
+ const hints = engineState(getState()).pendingAssistantRecallHints ?? [];
56
+ updateEngineState(getState, setState, (engine) => {
57
+ engine.pendingAssistantRecallHints = [];
58
+ engine.pendingAssistantRecallHintsRendered = false;
59
+ });
60
+ return hints;
61
+ },
62
+ getRecentRecallMemoryIds() {
63
+ return engineState(getState()).recentRecallMemoryIds ?? [];
64
+ },
65
+ restoreSession() {
66
+ throw new Error("remote runner session restore is not available");
67
+ },
68
+ };
69
+ }
70
+
71
+ function engineState(state) {
72
+ return state?.engine ?? {};
73
+ }
74
+
75
+ function updateEngineState(getState, setState, update) {
76
+ const state = getState();
77
+ if (!state) return;
78
+ const engine = { ...(state.engine ?? {}) };
79
+ update(engine);
80
+ setState({ ...state, engine });
81
+ }
@@ -51,6 +51,7 @@ export function createRuntimeUiClient(eventBus) {
51
51
  status: (text) => eventBus.emit({ type: "status", text }),
52
52
  debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
53
53
  recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
54
+ providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
54
55
  editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
55
56
  requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
56
57
  };
@@ -72,6 +73,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
72
73
  case "status": return ui.status?.(event.text);
73
74
  case "debug_lines": return writeDebugLines(ui, event.lines);
74
75
  case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
76
+ case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
75
77
  case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
76
78
  case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
77
79
  default: return undefined;
@@ -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"),
@@ -1,4 +1,5 @@
1
1
  import { createCommandExecTool } from "./command-exec-tool.mjs";
2
+ import { createCodeSearchTool } from "./code-search/tool.mjs";
2
3
  import { createContextStatsTool } from "./context-stats-tool.mjs";
3
4
  import { createEditFileTool } from "./file-edit-tool.mjs";
4
5
  import { createReadFileTool } from "./file-tools/read-file-tool.mjs";
@@ -11,9 +12,11 @@ import { createShellTools } from "../shell/tools.mjs";
11
12
  import { initImageGen } from "../image-gen/index.mjs";
12
13
  import { createSuperGrokTool } from "../supergrok/tool.mjs";
13
14
  import { createBrowserTools } from "../browser/tools/index.mjs";
15
+ import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
14
16
 
15
- export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
17
+ 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
18
  const commandExecTool = createCommandExecTool({ cwd });
19
+ const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
17
20
  const contextStatsTool = createContextStatsTool({ engine });
18
21
  const editFileTool = createEditFileTool({ engine, ui, lspService });
19
22
  const readFileTool = createReadFileTool({ engine });
@@ -29,12 +32,14 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
29
32
  screenTool,
30
33
  listWindowsTool,
31
34
  contextStatsTool,
35
+ codeSearchTool,
32
36
  commandExecTool,
33
37
  editFileTool,
34
38
  ...createShellTools(shellRuntime),
35
39
  ...memoryTools,
36
40
  ...mcpTools,
37
41
  ...webTools,
42
+ ...(lifecycle ? [createRuntimeRestartTool({ lifecycle })] : []),
38
43
  ...createBrowserTools({ stateRoot }),
39
44
  ...(authStorage ? [createSuperGrokTool({ authStorage, projectMarchDir })] : []),
40
45
  ...(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";