march-cli 0.1.35 → 0.1.37

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 (63) hide show
  1. package/package.json +1 -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/runner/provider-quota-runtime.mjs +38 -0
  19. package/src/agent/runner.mjs +14 -10
  20. package/src/agent/runtime/remote-runner-client.mjs +2 -0
  21. package/src/agent/runtime/runner-ipc-target.mjs +7 -0
  22. package/src/agent/runtime/runner-process-client.mjs +5 -0
  23. package/src/agent/runtime/runner-process-factory.mjs +5 -0
  24. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  25. package/src/agent/runtime/state/runner-state.mjs +1 -0
  26. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  27. package/src/agent/session/session-options.mjs +2 -1
  28. package/src/agent/tools.mjs +7 -1
  29. package/src/agent/turn/turn-events.mjs +41 -0
  30. package/src/agent/turn/turn-runner.mjs +5 -2
  31. package/src/cli/commands/registry/slash-command-registry.mjs +10 -7
  32. package/src/cli/commands/status-command.mjs +61 -35
  33. package/src/cli/input/history-store.mjs +65 -3
  34. package/src/cli/repl-loop.mjs +8 -6
  35. package/src/cli/startup/app-runtime.mjs +5 -29
  36. package/src/cli/startup/create-runtime-runner.mjs +4 -46
  37. package/src/cli/tui/input/history-navigation-controller.mjs +56 -0
  38. package/src/cli/turn/turn-input-preparer.mjs +0 -1
  39. package/src/cli/ui.mjs +9 -0
  40. package/src/context/engine.mjs +4 -2
  41. package/src/context/system-core/base.md +9 -1
  42. package/src/history/runner.mjs +11 -0
  43. package/src/history/store.mjs +129 -0
  44. package/src/history/tool.mjs +39 -0
  45. package/src/lsp/client.mjs +12 -5
  46. package/src/lsp/service.mjs +15 -3
  47. package/src/main.mjs +1 -2
  48. package/src/provider/quota/codex.mjs +278 -0
  49. package/src/provider/quota/index.mjs +46 -0
  50. package/src/provider/quota/transport-observer.mjs +99 -0
  51. package/src/web-ui/command.mjs +2 -2
  52. package/src/web-ui/runtime-host.mjs +7 -23
  53. package/src/web-ui/server.mjs +1 -0
  54. package/src/web-ui/session-manager.mjs +4 -2
  55. package/src/web-ui/src/components/AppShell.tsx +1 -0
  56. package/src/web-ui/src/components/RightSidebar.tsx +47 -2
  57. package/src/web-ui/src/model.ts +20 -0
  58. package/src/web-ui/src/runtime/client.ts +8 -1
  59. package/src/web-ui/src/runtime/useWebRuntime.ts +13 -1
  60. package/src/web-ui/src/styles/shell.css +10 -0
  61. package/src/web-ui/dist/assets/index-BUmhnID4.css +0 -1
  62. package/src/web-ui/dist/assets/index-CtuqTjcB.js +0 -1845
  63. package/src/web-ui/dist/index.html +0 -13
@@ -0,0 +1,56 @@
1
+ import { matchesKey } from "@earendil-works/pi-tui";
2
+
3
+ export function createHistoryNavigationController({ editor, requestRender, isAutocompleteOpen = () => false, hasOverlay = () => false } = {}) {
4
+ let draftText = null;
5
+
6
+ return {
7
+ handleInput(data) {
8
+ if (isAutocompleteOpen() || hasOverlay()) return undefined;
9
+
10
+ if (matchesKey(data, "alt+up")) return moveWithinInput(-1);
11
+ if (matchesKey(data, "alt+down")) return moveWithinInput(1);
12
+ if (matchesKey(data, "up")) return navigateHistory(-1);
13
+ if (matchesKey(data, "down")) return navigateHistory(1);
14
+
15
+ return undefined;
16
+ },
17
+ };
18
+
19
+ function navigateHistory(direction) {
20
+ const history = Array.isArray(editor?.history) ? editor.history : [];
21
+ const currentIndex = Number.isInteger(editor?.historyIndex) ? editor.historyIndex : -1;
22
+ const nextIndex = currentIndex - direction;
23
+ if (nextIndex < -1 || nextIndex >= history.length) return undefined;
24
+
25
+ if (currentIndex === -1) draftText = editor.getText?.() ?? "";
26
+ editor.lastAction = null;
27
+ editor.historyIndex = nextIndex;
28
+ setEditorTextPreservingHistory(nextIndex === -1 ? draftText ?? "" : history[nextIndex] ?? "");
29
+ if (nextIndex === -1) draftText = null;
30
+ requestRender?.();
31
+ return { consume: true };
32
+ }
33
+
34
+ function moveWithinInput(direction) {
35
+ editor.lastAction = null;
36
+ if (direction < 0) {
37
+ if (editor.isOnFirstVisualLine?.()) editor.moveToLineStart?.();
38
+ else editor.moveCursor?.(-1, 0);
39
+ } else {
40
+ if (editor.isOnLastVisualLine?.()) editor.moveToLineEnd?.();
41
+ else editor.moveCursor?.(1, 0);
42
+ }
43
+ requestRender?.();
44
+ return { consume: true };
45
+ }
46
+
47
+ function setEditorTextPreservingHistory(text) {
48
+ if (typeof editor.setTextInternal === "function") {
49
+ editor.setTextInternal(text);
50
+ return;
51
+ }
52
+ const historyIndex = editor.historyIndex;
53
+ editor.setText?.(text);
54
+ editor.historyIndex = historyIndex;
55
+ }
56
+ }
@@ -5,7 +5,6 @@ import { formatMessageAttachmentsForDisplay } from "../../session/attachment-dis
5
5
  import { formatShellHints } from "../../shell/hints.mjs";
6
6
 
7
7
  export function prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState = null }) {
8
- memoryStore.beginTurn();
9
8
  const engine = runner.engine ?? {};
10
9
  const carryoverAlreadyRendered = engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
11
10
  const carryoverRecallHints = engine.takePendingAssistantRecallHints?.() ?? [];
package/src/cli/ui.mjs CHANGED
@@ -16,6 +16,7 @@ import { showEditorSelectList } from "./tui/select/editor-select-list.mjs";
16
16
  import { StatusBar } from "./tui/status/status-bar.mjs";
17
17
  import { MainPaneLayout } from "./tui/layout/main-pane-layout.mjs";
18
18
  import { SafeRenderBoundary } from "./tui/layout/safe-render-boundary.mjs";
19
+ import { createHistoryNavigationController } from "./tui/input/history-navigation-controller.mjs";
19
20
  import { createMouseSelectionController } from "./tui/input/mouse-selection-controller.mjs";
20
21
  import { ScreenSelection } from "./tui/selection-screen.mjs";
21
22
  import { writeEditDiff } from "./tui/tui-diff-rendering.mjs";
@@ -68,6 +69,12 @@ export function createTuiUI({
68
69
  const retryStatus = createRetryStatusController({ output, requestRender, stopSpinner: spinnerStatus.stop });
69
70
  const shellDrawerControls = createShellDrawerControls({ shellDrawer, output, requestRender });
70
71
  const mouseSelectionController = createMouseSelectionController({ terminal, output, shellDrawer, shellDrawerControls, selection, writeClipboard, requestRender });
72
+ const historyNavigationController = createHistoryNavigationController({
73
+ editor,
74
+ requestRender,
75
+ isAutocompleteOpen: () => editor.isShowingAutocomplete(),
76
+ hasOverlay: () => tui.hasOverlay(),
77
+ });
71
78
 
72
79
  let onEscapeHandler = null, onCtrlCHandler = null, onShiftTabHandler = null;
73
80
  let onCtrlTHandler = null, onCtrlLHandler = null, onPasteImageHandler = null, onToggleModeHandler = null;
@@ -108,6 +115,8 @@ export function createTuiUI({
108
115
  requestRender();
109
116
  return { consume: true };
110
117
  }
118
+ const historyNavigationResult = historyNavigationController.handleInput(data);
119
+ if (historyNavigationResult) return historyNavigationResult;
111
120
  });
112
121
  terminal.write("\x1b[?1049h");
113
122
  terminal.write("\x1b[?1002h\x1b[?1006h");
@@ -71,18 +71,20 @@ export class ContextEngine {
71
71
  }
72
72
 
73
73
  recordTurn({ userMessage, assistantMessage, assistantContext = "", userRecallHints = [], assistantRecallHints = [] }) {
74
- this.turns.push({
74
+ const turn = {
75
75
  index: this.turns.length + 1,
76
76
  userMessage,
77
77
  assistantMessage: assistantMessage ?? "",
78
78
  assistantContext: assistantContext ?? "",
79
79
  userRecallHints,
80
80
  assistantRecallHints,
81
- });
81
+ };
82
+ this.turns.push(turn);
82
83
  if (this.turns.length > this.maxTurns) {
83
84
  const keep = Math.max(1, this.maxTurns - this.trimBatch);
84
85
  this.turns = this.turns.slice(-keep);
85
86
  }
87
+ return turn;
86
88
  }
87
89
 
88
90
  getRecentRecallMemoryIds() {
@@ -69,7 +69,10 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
69
69
 
70
70
  <editing_contract>
71
71
  - Use read(path) for file inspection with 1-based line numbers.
72
- - Use grep(pattern), find(pattern), and ls(path) to explore the project before editing.
72
+ - Use code_search first when locating unknown implementations, responsibility boundaries, cross-module flows, or concept-level behavior.
73
+ - Use grep(pattern) and find(pattern) for exact symbol, string, filename, or call-site confirmation.
74
+ - Use ls(path) to inspect directory shape when structure matters.
75
+ - Treat code_search as a semantic map, not proof; verify important results with grep/read before editing or concluding.
73
76
  - Prefer dedicated read/search/edit tools over shell commands for file inspection and modification.
74
77
  - Use command_exec for one-shot commands. Use terminal_* only for interactive programs, long-running processes, or when preserving terminal state matters.
75
78
  - Keep the working directory stable; use paths instead of cd unless the user asks otherwise.
@@ -103,6 +106,11 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
103
106
  - Use memory_save() to create memories or update whole metadata fields on an existing memory. Before creating a new memory, first search/open related memories and merge updates into an existing memory when they share the same topic, project, or decision thread; prefer modifying the existing memory file over creating a scattered new one. Tags are the primary retrieval key for future recall. Prefer lowercase-kebab-case tags like 'march-cli', 'tooling', 'permissions'.
104
107
  - When learning multiple related external workflows or skills, maintain memory as an evolving domain library: start with the specific source name when only one item exists, then rename and rewrite the memory title/description as the scope grows; merge new related learnings into the same memory, preserving each source's unique traits while distilling reusable principles.
105
108
  - Distinguish "migrating a Skill to memory" from "learning a Skill": migration preserves the complete Skill folder under memory_root/skills/ and creates a memory entry as its index; that memory should describe what the Skill is for and reference the copied Skill folder path so future recall knows how to use it. Learning only reads and internalizes the Skill's methods, scenarios, and principles into ordinary memory without copying source files. Infer the action from the user's wording, and ask when ambiguous.
109
+ - Do not proactively modify the agent profile. Update `agent.md` only when the user explicitly asks to change persistent agent behavior.
110
+ - Proactively maintain the user profile when stable, reusable user preferences, working style, goals, or identity signals appear in conversation.
111
+ - For user profile updates, distinguish explicit facts from inferred preferences. Write explicit facts directly; write inferred items as preferences or tendencies, and avoid overstating confidence.
112
+ - Do not store transient task details, sensitive information, or one-off opinions in the user profile. Use memory for project-specific reusable knowledge and current conversation for short-lived context.
113
+ - When a user profile update is non-obvious or potentially sensitive, ask before writing; otherwise update it as part of normal task completion and mention it briefly in the final summary.
106
114
  - Unlike recall blocks, this system-core center is always visible in every model call. Only update the center for instructions that must always be followed; use memory for contextual, project-specific, or recall-dependent knowledge.
107
115
  - If execution takes a meaningful detour, create or update a memory after the task. A detour means the initial plan or assumption failed, multiple approaches were tried, and the final successful path contains reusable project knowledge. Record the failed assumption, what was tried, and the successful approach. Prefer updating an existing related memory over creating a new one.
108
116
  </memory_system>
@@ -0,0 +1,11 @@
1
+ import { join } from "node:path";
2
+ import { HistoryStore } from "./store.mjs";
3
+
4
+ export function createRunnerHistoryStore({ stateRoot, cwd } = {}) {
5
+ if (!stateRoot) return null;
6
+ return new HistoryStore({ root: join(stateRoot, "history"), cwd });
7
+ }
8
+
9
+ export function appendRunnerTurnHistory({ store, turn, sessionStats, modelId, provider }) {
10
+ return store?.appendTurn({ turn, sessionStats, runtime: { modelId, provider } });
11
+ }
@@ -0,0 +1,129 @@
1
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { basename, join } from "node:path";
4
+ import { searchMarkdownRoot } from "../memory/search.mjs";
5
+
6
+ const DEFAULT_HISTORY_LIMIT = 20;
7
+ const MAX_HISTORY_LIMIT = 50;
8
+
9
+ export class HistoryStore {
10
+ constructor({ root, cwd, now = () => new Date() } = {}) {
11
+ if (!root) throw new Error("history root is required");
12
+ this.root = root;
13
+ this.cwd = cwd;
14
+ this.now = now;
15
+ }
16
+
17
+ appendTurn({ turn, sessionStats = {}, runtime = {} } = {}) {
18
+ if (!turn) return null;
19
+ const sessionId = sanitizeId(sessionStats.sessionId ?? sessionStats.sessionFile ?? "session");
20
+ const filePath = join(this.#projectDir(), `${sessionId}.md`);
21
+ mkdirSync(this.#projectDir(), { recursive: true });
22
+ if (!existsSync(filePath)) writeFileSync(filePath, this.#fileHeader({ sessionId, sessionStats }), "utf8");
23
+ appendFileSync(filePath, this.#formatTurn({ turn, sessionId, sessionStats, runtime }), "utf8");
24
+ return filePath;
25
+ }
26
+
27
+ searchRipgrep(query, { allProjects = false, sessionId = null, limit = DEFAULT_HISTORY_LIMIT, context = 2, syntax = "regex", case: caseMode = "smart" } = {}) {
28
+ const root = allProjects ? this.root : this.#projectDir();
29
+ const glob = sessionId ? [`**/${sanitizeId(sessionId)}.md`] : [];
30
+ return searchMarkdownRoot({
31
+ root,
32
+ query,
33
+ limit: clampInt(limit, 1, MAX_HISTORY_LIMIT, DEFAULT_HISTORY_LIMIT),
34
+ context,
35
+ syntax,
36
+ caseMode,
37
+ glob,
38
+ });
39
+ }
40
+
41
+ #projectDir() {
42
+ return join(this.root, "projects", cwdHash(this.cwd));
43
+ }
44
+
45
+ #fileHeader({ sessionId, sessionStats }) {
46
+ return [
47
+ `# March History · ${sessionId}`,
48
+ "",
49
+ `- cwd: ${this.cwd}`,
50
+ `- project: ${basename(this.cwd) || this.cwd}`,
51
+ sessionStats.sessionFile ? `- session_file: ${sessionStats.sessionFile}` : null,
52
+ "",
53
+ ].filter(Boolean).join("\n");
54
+ }
55
+
56
+ #formatTurn({ turn, sessionId, sessionStats, runtime }) {
57
+ const now = this.now().toISOString();
58
+ return [
59
+ `\n## Turn ${turn.index ?? "?"} · ${now}`,
60
+ "",
61
+ "metadata:",
62
+ `- session: ${sessionId}`,
63
+ `- cwd: ${this.cwd}`,
64
+ runtime.provider ? `- provider: ${runtime.provider}` : null,
65
+ runtime.modelId ? `- model: ${runtime.modelId}` : null,
66
+ sessionStats.sessionName ? `- session_name: ${sessionStats.sessionName}` : null,
67
+ "",
68
+ "### User",
69
+ safeBlock(turn.userMessage),
70
+ formatRecallSection("User memory recall", turn.userRecallHints),
71
+ "### Assistant",
72
+ safeBlock(turn.assistantMessage),
73
+ turn.thinking ? ["### Thinking", safeBlock(turn.thinking)].join("\n") : null,
74
+ "### Tool calls",
75
+ formatToolCalls(turn.toolCalls),
76
+ formatRecallSection("Assistant memory recall", turn.assistantRecallHints),
77
+ "",
78
+ ].filter(Boolean).join("\n");
79
+ }
80
+ }
81
+
82
+ function formatToolCalls(calls = []) {
83
+ if (!Array.isArray(calls) || calls.length === 0) return "(none)";
84
+ return calls.map((call) => {
85
+ const lines = [`- ${call.name ?? "unknown"} status=${call.status ?? "unknown"}`];
86
+ const args = JSON.stringify(call.args ?? null);
87
+ if (args && args !== "null") lines.push(` args: ${args}`);
88
+ if (call.status === "failed") {
89
+ lines.push(" error:");
90
+ if (call.error?.message) lines.push(` message: ${escapeSingleLine(call.error.message)}`);
91
+ if (call.error?.details) lines.push(` details: ${JSON.stringify(call.error.details)}`);
92
+ if (call.error?.excerpt) lines.push(" excerpt:", fence(call.error.excerpt));
93
+ }
94
+ return lines.join("\n");
95
+ }).join("\n");
96
+ }
97
+
98
+ function formatRecallSection(title, hints = []) {
99
+ if (!Array.isArray(hints) || hints.length === 0) return null;
100
+ return [`### ${title}`, ...hints.map((hint) => `- ${hint.id ?? "unknown"} | ${hint.name ?? hint.title ?? ""} | ${hint.description ?? ""}`)].join("\n");
101
+ }
102
+
103
+ function safeBlock(value) {
104
+ const text = String(value ?? "").trim();
105
+ return text || "(empty)";
106
+ }
107
+
108
+ function fence(value) {
109
+ return " ```text\n" + String(value ?? "").trimEnd() + "\n ```";
110
+ }
111
+
112
+ function sanitizeId(value) {
113
+ const raw = String(value ?? "session").trim() || "session";
114
+ return raw.replace(/[^a-zA-Z0-9_.-]+/g, "_").slice(0, 120) || "session";
115
+ }
116
+
117
+ function cwdHash(cwd) {
118
+ return createHash("sha1").update(String(cwd ?? "")).digest("hex").slice(0, 16);
119
+ }
120
+
121
+ function escapeSingleLine(value) {
122
+ return JSON.stringify(String(value ?? "").replace(/\s+/g, " "));
123
+ }
124
+
125
+ function clampInt(value, min, max, fallback) {
126
+ const number = Number(value);
127
+ if (!Number.isFinite(number)) return fallback;
128
+ return Math.min(max, Math.max(min, Math.floor(number)));
129
+ }
@@ -0,0 +1,39 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { toolText } from "../agent/tool-result.mjs";
4
+
5
+ export function createHistorySearchTool({ store } = {}) {
6
+ if (!store) return null;
7
+ return defineTool({
8
+ name: "history_search",
9
+ label: "History Search",
10
+ description: "Search archived March turn history with ripgrep. Use it when you need details from previous sessions or earlier turns. History stores user/assistant text, visible thinking, tool call metadata, memory recall hints, and failed tool error excerpts; successful tool results are not stored.",
11
+ parameters: Type.Object({
12
+ query: Type.String({ description: "Ripgrep query/pattern to search in archived turn history" }),
13
+ allProjects: Type.Optional(Type.Boolean({ description: "Search all project histories instead of the current cwd history. Default false." })),
14
+ sessionId: Type.Optional(Type.String({ description: "Limit search to a specific session id when known" })),
15
+ syntax: Type.Optional(Type.Union([Type.Literal("regex"), Type.Literal("literal")], { description: "Pattern syntax. Default: regex" })),
16
+ case: Type.Optional(Type.Union([Type.Literal("smart"), Type.Literal("sensitive"), Type.Literal("insensitive")], { description: "Case matching mode. Default: smart" })),
17
+ context: Type.Optional(Type.Number({ description: "Context lines around each match. Default: 2" })),
18
+ limit: Type.Optional(Type.Number({ description: "Maximum matches to return. Default: 20, max: 50" })),
19
+ }),
20
+ execute: async (_toolCallId, params) => {
21
+ try {
22
+ const results = store.searchRipgrep(params.query, params);
23
+ if (results.length === 0) return toolText(`history_search found no matches for ${JSON.stringify(params.query)}.`);
24
+ return toolText(formatHistorySearchResults(results), { results });
25
+ } catch (err) {
26
+ return toolText(`Error: ${err.message}`, { error: true });
27
+ }
28
+ },
29
+ });
30
+ }
31
+
32
+ function formatHistorySearchResults(results) {
33
+ const lines = [`history_search found ${results.length} match${results.length === 1 ? "" : "es"}.`];
34
+ results.forEach((result, index) => {
35
+ lines.push("", `[${index + 1}] ${result.path}:${result.line}`);
36
+ if (result.excerpt?.text) lines.push(result.excerpt.text);
37
+ });
38
+ return lines.join("\n");
39
+ }
@@ -73,13 +73,14 @@ export function languageIdForPath(path) {
73
73
  }
74
74
 
75
75
  export class LspClient {
76
- constructor({ serverId, command, args = [], cwd, initialization = {}, store }) {
76
+ constructor({ serverId, command, args = [], cwd, initialization = {}, store, onStatusChange = null }) {
77
77
  this.serverId = serverId;
78
78
  this.command = command;
79
79
  this.args = args;
80
80
  this.cwd = cwd;
81
81
  this.initialization = initialization;
82
82
  this.store = store;
83
+ this.onStatusChange = onStatusChange;
83
84
  this.status = "starting";
84
85
  this.process = null;
85
86
  this.buffer = Buffer.alloc(0);
@@ -98,7 +99,7 @@ export class LspClient {
98
99
  });
99
100
  this.process.stdout.on("data", (chunk) => this.#onData(chunk));
100
101
  this.process.on("exit", () => {
101
- this.status = "failed";
102
+ this.#setStatus("failed");
102
103
  for (const pending of this.pending.values()) pending.reject(new Error("LSP exited"));
103
104
  this.pending.clear();
104
105
  });
@@ -124,7 +125,7 @@ export class LspClient {
124
125
  }), INITIALIZE_TIMEOUT_MS);
125
126
  this.syncKind = getSyncKind(initialized?.capabilities);
126
127
  this.#notify("initialized", {});
127
- this.status = "ready";
128
+ this.#setStatus("ready");
128
129
  }
129
130
 
130
131
  touchFile(path) {
@@ -132,7 +133,7 @@ export class LspClient {
132
133
  const text = readFileSync(path, "utf8");
133
134
  const uri = pathToFileURL(path).href;
134
135
  const existing = this.documents.get(path);
135
- this.status = "busy";
136
+ this.#setStatus("busy");
136
137
  if (existing) {
137
138
  const version = existing.version + 1;
138
139
  this.documents.set(path, { version, text });
@@ -199,7 +200,7 @@ export class LspClient {
199
200
  uri: message.params?.uri,
200
201
  diagnostics: message.params?.diagnostics ?? [],
201
202
  });
202
- this.status = "idle";
203
+ this.#setStatus("idle");
203
204
  return;
204
205
  }
205
206
 
@@ -208,6 +209,12 @@ export class LspClient {
208
209
  }
209
210
  }
210
211
 
212
+ #setStatus(status) {
213
+ if (this.status === status) return;
214
+ this.status = status;
215
+ this.onStatusChange?.({ id: this.serverId, root: this.cwd, status });
216
+ }
217
+
211
218
  #requestResult(method) {
212
219
  if (method === "workspace/configuration") return [];
213
220
  if (method === "workspace/workspaceFolders") return [{ name: "workspace", uri: pathToFileURL(this.cwd).href }];
@@ -3,9 +3,10 @@ import { LspDiagnosticStore } from "./diagnostic-store.mjs";
3
3
  import { resolveLspServerStatus } from "./servers.mjs";
4
4
 
5
5
  export class LspService {
6
- constructor({ cwd, onEvent = null }) {
6
+ constructor({ cwd, onEvent = null, onStatusChange = null }) {
7
7
  this.cwd = cwd;
8
8
  this.onEvent = onEvent;
9
+ this.onStatusChange = onStatusChange;
9
10
  this.store = new LspDiagnosticStore();
10
11
  this.clients = new Map();
11
12
  this.spawning = new Map();
@@ -19,6 +20,7 @@ export class LspService {
19
20
  if (result.status === "unavailable") {
20
21
  this.unavailable.set(result.id, result);
21
22
  this.#emitOnce(`unavailable:${result.id}:${result.reason}`, result);
23
+ this.#emitStatusChange(result);
22
24
  return result;
23
25
  }
24
26
 
@@ -34,7 +36,9 @@ export class LspService {
34
36
  return { status: "starting", id: server.id, root: server.root };
35
37
  }
36
38
 
37
- this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root, managed: server.managed });
39
+ const startingEvent = { status: "starting", id: server.id, root: server.root, managed: server.managed };
40
+ this.#emitOnce(`starting:${key}`, startingEvent);
41
+ this.#emitStatusChange(startingEvent);
38
42
  const task = this.#startClient(server, key).then((client) => {
39
43
  client?.touchFile(path);
40
44
  return client;
@@ -76,18 +80,22 @@ export class LspService {
76
80
  cwd: server.root,
77
81
  initialization: server.initialization,
78
82
  store: this.store,
83
+ onStatusChange: (event) => this.#emitStatusChange(event),
79
84
  });
80
85
  try {
81
86
  await client.start();
82
87
  this.clients.set(key, client);
83
88
  this.unavailable.delete(server.id);
84
- this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root, managed: server.managed });
89
+ const attachedEvent = { status: "attached", id: server.id, root: server.root, managed: server.managed };
90
+ this.#emitOnce(`attached:${key}`, attachedEvent);
91
+ this.#emitStatusChange(attachedEvent);
85
92
  return client;
86
93
  } catch (err) {
87
94
  client.status = "failed";
88
95
  const event = { status: "failed", id: server.id, root: server.root, reason: err.message };
89
96
  this.unavailable.set(server.id, event);
90
97
  this.#emitOnce(`failed:${key}:${err.message}`, event);
98
+ this.#emitStatusChange(event);
91
99
  return null;
92
100
  }
93
101
  }
@@ -97,6 +105,10 @@ export class LspService {
97
105
  this.announced.add(key);
98
106
  this.onEvent?.(event);
99
107
  }
108
+
109
+ #emitStatusChange(event) {
110
+ this.onStatusChange?.(event);
111
+ }
100
112
  }
101
113
 
102
114
  function summarizeStatus(servers) {
package/src/main.mjs CHANGED
@@ -26,13 +26,12 @@ export async function run(argv) {
26
26
 
27
27
  const config = loadConfig(cwd);
28
28
  const stateRoot = join(homedir(), ".march");
29
- const useRuntimeProcess = process.env.MARCH_RUNTIME_PROCESS !== "0";
30
29
  installNetworkEnvironment(config.network);
31
30
 
32
31
  const earlyCommand = await runEarlyCliCommand(args, { config, cwd, stateRoot });
33
32
  if (earlyCommand.handled) return earlyCommand.code;
34
33
 
35
- const app = await createCliAppRuntime({ args, config, cwd, argv, stateRoot, useRuntimeProcess });
34
+ const app = await createCliAppRuntime({ args, config, cwd, argv, stateRoot });
36
35
  if (!app.ok) return app.code;
37
36
 
38
37
  const gatewayDaemonCommand = await maybeRunGatewayDaemonCommand(args, {