march-cli 0.1.8 → 0.1.9
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.
- package/package.json +1 -1
- package/src/agent/file-edit-tool.mjs +2 -2
- package/src/agent/runner.mjs +3 -1
- package/src/cli/commands/status-command.mjs +46 -0
- package/src/cli/status-line-updater.mjs +1 -0
- package/src/{context/diagnostics.mjs → lsp/diagnostics-format.mjs} +3 -3
- package/src/lsp/servers.mjs +49 -16
- package/src/lsp/service.mjs +55 -10
- package/src/lsp/status-message.mjs +8 -0
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
import { toolText } from "./tool-result.mjs";
|
|
6
6
|
import { applyReplaceTextPatch, applyReplaceRangePatch } from "./editing/diff-apply.mjs";
|
|
7
7
|
import { formatAppliedDiff, formatDiff } from "./editing/diff-format.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { formatLspDiagnosticsForPath } from "../lsp/diagnostics-format.mjs";
|
|
9
9
|
|
|
10
10
|
export { formatDiff } from "./editing/diff-format.mjs";
|
|
11
11
|
|
|
@@ -117,7 +117,7 @@ async function waitForDiagnosticsForPath({ lspService, path, timeoutMs, interval
|
|
|
117
117
|
if (!lspService?.snapshot || !path) return "";
|
|
118
118
|
const deadline = Date.now() + timeoutMs;
|
|
119
119
|
for (;;) {
|
|
120
|
-
const diagnostics =
|
|
120
|
+
const diagnostics = formatLspDiagnosticsForPath({ snapshot: lspService.snapshot(), path });
|
|
121
121
|
if (diagnostics) return diagnostics;
|
|
122
122
|
const remaining = deadline - Date.now();
|
|
123
123
|
if (remaining <= 0) return "";
|
package/src/agent/runner.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { ContextEngine } from "../context/engine.mjs";
|
|
|
8
8
|
import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs";
|
|
9
9
|
import { syncPiSessionSidecar } from "../session/sidecar-sync.mjs";
|
|
10
10
|
import { LspService } from "../lsp/service.mjs";
|
|
11
|
+
import { formatLspServiceEvent } from "../lsp/status-message.mjs";
|
|
11
12
|
import { formatRecallHints } from "../memory/markdown-store.mjs";
|
|
12
13
|
import { appendProviderUserMessage, estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
|
|
13
14
|
import { resolveInitialModel, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
@@ -48,7 +49,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
48
49
|
compaction: { enabled: false },
|
|
49
50
|
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
50
51
|
});
|
|
51
|
-
const lspService = new LspService({ cwd });
|
|
52
|
+
const lspService = new LspService({ cwd, onEvent: (event) => ui.status?.(formatLspServiceEvent(event)) });
|
|
52
53
|
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, centerMemoryPath, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
53
54
|
const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
|
|
54
55
|
const sessionBinding = createSessionBinding(null);
|
|
@@ -218,6 +219,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
218
219
|
},
|
|
219
220
|
getExtensionDiagnostics() { return runtimeHost?.getDiagnostics?.() ?? []; },
|
|
220
221
|
getExtensionLifecycleState() { return lifecycleAdapter.getState(); },
|
|
222
|
+
getLspStatus() { return lspService.snapshot(); },
|
|
221
223
|
async switchPiSession(sessionPath) {
|
|
222
224
|
if (!runtimeHost) throw new Error("pi runtime host is not enabled");
|
|
223
225
|
nextTurnContextMode = "rebuild";
|
|
@@ -31,6 +31,7 @@ export function statusBarLine({
|
|
|
31
31
|
mode = MODES.DO,
|
|
32
32
|
contextTokens = null,
|
|
33
33
|
activity = null,
|
|
34
|
+
lspStatus = null,
|
|
34
35
|
}) {
|
|
35
36
|
return formatStatusBarLine({
|
|
36
37
|
engine: runner.engine,
|
|
@@ -43,6 +44,7 @@ export function statusBarLine({
|
|
|
43
44
|
mode,
|
|
44
45
|
contextTokens,
|
|
45
46
|
activity,
|
|
47
|
+
lspStatus,
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -80,6 +82,7 @@ export function formatStatusBarLine({
|
|
|
80
82
|
mode = MODES.DO,
|
|
81
83
|
contextTokens = null,
|
|
82
84
|
activity = null,
|
|
85
|
+
lspStatus = null,
|
|
83
86
|
}) {
|
|
84
87
|
const model = engine.modelId || "model?";
|
|
85
88
|
const thinking = engine.thinkingLevel || "thinking?";
|
|
@@ -91,6 +94,8 @@ export function formatStatusBarLine({
|
|
|
91
94
|
const modeSegment = `${mode === MODES.DISCUSS ? WARN : OK}${formatModeLabel(mode)}`;
|
|
92
95
|
const runtime = `${C.cyan}${model}${DIM}·${thinking}`;
|
|
93
96
|
const segments = [modeSegment, runtime];
|
|
97
|
+
const lspText = formatLspSegment(lspStatus);
|
|
98
|
+
if (lspText) segments.push(`${C.fg250}${lspText}`);
|
|
94
99
|
const activityText = formatActivitySegment(activity);
|
|
95
100
|
if (activityText) segments.push(`${C.fg250}${activityText}`);
|
|
96
101
|
const compactTokens = formatCompactTokenCount(contextTokens);
|
|
@@ -100,6 +105,42 @@ export function formatStatusBarLine({
|
|
|
100
105
|
return `${inner}${R}`;
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
export function formatLspSegment(lspStatus) {
|
|
109
|
+
if (!lspStatus) return "";
|
|
110
|
+
const servers = lspStatus.servers ?? [];
|
|
111
|
+
const visible = servers.filter((server) => server.id);
|
|
112
|
+
if (visible.length === 0) return "lsp:off";
|
|
113
|
+
const parts = buildLspStatusParts(visible);
|
|
114
|
+
if (parts.length === 0) return "lsp:off";
|
|
115
|
+
return `lsp:${parts.join(",")}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildLspStatusParts(servers) {
|
|
119
|
+
const byId = new Map();
|
|
120
|
+
for (const server of servers) {
|
|
121
|
+
const id = shortLspId(server.id);
|
|
122
|
+
byId.set(id, mergeLspStatus(byId.get(id), server.status));
|
|
123
|
+
}
|
|
124
|
+
return [...byId.entries()]
|
|
125
|
+
.filter(([, status]) => status !== "unavailable")
|
|
126
|
+
.map(([id, status]) => `${id}${formatLspStatusMark(status)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mergeLspStatus(current, next) {
|
|
130
|
+
const rank = { failed: 4, starting: 3, busy: 2, ready: 1, idle: 1, unavailable: 0 };
|
|
131
|
+
if (!current) return next ?? "unavailable";
|
|
132
|
+
const currentRank = rank[current] ?? 0;
|
|
133
|
+
const nextRank = rank[next] ?? 0;
|
|
134
|
+
return nextRank > currentRank ? next : current;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatLspStatusMark(status) {
|
|
138
|
+
if (status === "failed") return "!";
|
|
139
|
+
if (status === "starting") return "…";
|
|
140
|
+
if (status === "unavailable") return "";
|
|
141
|
+
return "✓";
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
function formatActivitySegment(activity) {
|
|
104
145
|
if (!activity) return "";
|
|
105
146
|
const label = String(activity.label ?? "").trim();
|
|
@@ -107,6 +148,11 @@ function formatActivitySegment(activity) {
|
|
|
107
148
|
return [frame, label].filter(Boolean).join(" ");
|
|
108
149
|
}
|
|
109
150
|
|
|
151
|
+
function shortLspId(id) {
|
|
152
|
+
if (id === "typescript") return "ts";
|
|
153
|
+
return String(id ?? "?");
|
|
154
|
+
}
|
|
155
|
+
|
|
110
156
|
export function formatCompactTokenCount(tokens) {
|
|
111
157
|
const value = Number(tokens);
|
|
112
158
|
if (!Number.isFinite(value) || value <= 0) return "";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const MAX_DIAGNOSTICS = 20;
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function formatLspDiagnostics({ snapshot } = {}) {
|
|
4
4
|
const diagnostics = snapshot?.diagnostics ?? [];
|
|
5
5
|
if (diagnostics.length === 0) return "[diagnostics]";
|
|
6
6
|
|
|
@@ -17,12 +17,12 @@ export function buildDiagnosticsLayer({ snapshot } = {}) {
|
|
|
17
17
|
return lines.join("\n");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function
|
|
20
|
+
export function formatLspDiagnosticsForPath({ snapshot, path } = {}) {
|
|
21
21
|
const targetPath = String(path ?? "");
|
|
22
22
|
if (!targetPath) return "";
|
|
23
23
|
const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => diagnostic.path === targetPath);
|
|
24
24
|
if (diagnostics.length === 0) return "";
|
|
25
|
-
return
|
|
25
|
+
return formatLspDiagnostics({
|
|
26
26
|
snapshot: {
|
|
27
27
|
...snapshot,
|
|
28
28
|
diagnostics,
|
package/src/lsp/servers.mjs
CHANGED
|
@@ -11,7 +11,11 @@ const LSP_SERVERS = [
|
|
|
11
11
|
rootMarkers: NODE_ROOT_MARKERS,
|
|
12
12
|
command: ["vue-language-server"],
|
|
13
13
|
args: ["--stdio"],
|
|
14
|
-
initialization: () =>
|
|
14
|
+
initialization: ({ root, workspaceRoot }) => {
|
|
15
|
+
const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
|
|
16
|
+
return tsdk ? { typescript: { tsdk } } : null;
|
|
17
|
+
},
|
|
18
|
+
missingInitialization: "missing project typescript SDK",
|
|
15
19
|
},
|
|
16
20
|
{
|
|
17
21
|
id: "typescript",
|
|
@@ -20,10 +24,10 @@ const LSP_SERVERS = [
|
|
|
20
24
|
command: ["typescript-language-server"],
|
|
21
25
|
args: ["--stdio"],
|
|
22
26
|
initialization: ({ root, workspaceRoot }) => {
|
|
23
|
-
const tsserver =
|
|
24
|
-
|
|
25
|
-
return { tsserver: { path: tsserver } };
|
|
27
|
+
const tsserver = resolveTypeScriptServer({ root, workspaceRoot });
|
|
28
|
+
return tsserver ? { tsserver: { path: tsserver } } : null;
|
|
26
29
|
},
|
|
30
|
+
missingInitialization: "missing project typescript/tsserver.js",
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
33
|
id: "python",
|
|
@@ -68,10 +72,10 @@ const LSP_SERVERS = [
|
|
|
68
72
|
command: ["astro-ls", "@astrojs/language-server"],
|
|
69
73
|
args: ["--stdio"],
|
|
70
74
|
initialization: ({ root, workspaceRoot }) => {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
return { typescript: { tsdk: dirname(tsserver) } };
|
|
75
|
+
const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
|
|
76
|
+
return tsdk ? { typescript: { tsdk } } : null;
|
|
74
77
|
},
|
|
78
|
+
missingInitialization: "missing project typescript SDK",
|
|
75
79
|
},
|
|
76
80
|
{
|
|
77
81
|
id: "yaml",
|
|
@@ -126,23 +130,30 @@ const LSP_SERVERS = [
|
|
|
126
130
|
];
|
|
127
131
|
|
|
128
132
|
export function resolveLspServer({ filePath, workspaceRoot }) {
|
|
133
|
+
const result = resolveLspServerStatus({ filePath, workspaceRoot });
|
|
134
|
+
return result.status === "available" ? result.server : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resolveLspServerStatus({ filePath, workspaceRoot }) {
|
|
129
138
|
const ext = extensionOf(filePath);
|
|
130
139
|
const def = LSP_SERVERS.find((server) => server.extensions.includes(ext));
|
|
131
|
-
if (!def) return
|
|
140
|
+
if (!def) return { status: "unsupported", extension: ext };
|
|
132
141
|
|
|
133
142
|
const root = def.rootMarkers.length > 0
|
|
134
143
|
? findNearestRoot(dirname(filePath), workspaceRoot, def.rootMarkers) ?? workspaceRoot
|
|
135
144
|
: workspaceRoot;
|
|
136
145
|
const command = findCommand(def.command, { root, workspaceRoot });
|
|
137
|
-
if (!command)
|
|
146
|
+
if (!command) {
|
|
147
|
+
return { status: "unavailable", id: def.id, root, reason: `missing ${def.command[0]}` };
|
|
148
|
+
}
|
|
149
|
+
|
|
138
150
|
const initialization = def.initialization?.({ root, workspaceRoot }) ?? {};
|
|
139
|
-
if (initialization === null)
|
|
151
|
+
if (initialization === null) {
|
|
152
|
+
return { status: "unavailable", id: def.id, root, reason: def.missingInitialization ?? "missing SDK" };
|
|
153
|
+
}
|
|
140
154
|
return {
|
|
141
|
-
|
|
142
|
-
command,
|
|
143
|
-
args: def.args,
|
|
144
|
-
root,
|
|
145
|
-
initialization,
|
|
155
|
+
status: "available",
|
|
156
|
+
server: { id: def.id, command, args: def.args, root, initialization },
|
|
146
157
|
};
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -160,7 +171,7 @@ function findCommand(names, { root, workspaceRoot }) {
|
|
|
160
171
|
|
|
161
172
|
function findBin(name, { root, workspaceRoot }) {
|
|
162
173
|
const names = platformCommandNames(name);
|
|
163
|
-
for (const base of [root, workspaceRoot]) {
|
|
174
|
+
for (const base of uniquePaths([root, workspaceRoot])) {
|
|
164
175
|
for (const bin of names) {
|
|
165
176
|
const candidate = join(base, "node_modules", ".bin", bin);
|
|
166
177
|
if (existsSync(candidate)) return candidate;
|
|
@@ -185,6 +196,24 @@ function findOnPath(names) {
|
|
|
185
196
|
return null;
|
|
186
197
|
}
|
|
187
198
|
|
|
199
|
+
function resolveTypeScriptServer({ root, workspaceRoot }) {
|
|
200
|
+
return resolveModuleFromRoots("typescript/lib/tsserver.js", { root, workspaceRoot });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveTypeScriptSdk({ root, workspaceRoot }) {
|
|
204
|
+
const serverLibrary = resolveModuleFromRoots("typescript/lib/tsserverlibrary.js", { root, workspaceRoot });
|
|
205
|
+
const tsserver = serverLibrary ?? resolveTypeScriptServer({ root, workspaceRoot });
|
|
206
|
+
return tsserver ? dirname(tsserver) : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveModuleFromRoots(id, { root, workspaceRoot }) {
|
|
210
|
+
for (const base of uniquePaths([root, workspaceRoot])) {
|
|
211
|
+
const hit = resolveModule(id, base);
|
|
212
|
+
if (hit) return hit;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
188
217
|
function resolveModule(id, base) {
|
|
189
218
|
try {
|
|
190
219
|
return createRequire(join(base, "package.json")).resolve(id);
|
|
@@ -193,6 +222,10 @@ function resolveModule(id, base) {
|
|
|
193
222
|
}
|
|
194
223
|
}
|
|
195
224
|
|
|
225
|
+
function uniquePaths(paths) {
|
|
226
|
+
return [...new Set(paths.map((path) => resolve(path)))];
|
|
227
|
+
}
|
|
228
|
+
|
|
196
229
|
function findNearestRoot(start, stop, markers) {
|
|
197
230
|
let dir = resolve(start);
|
|
198
231
|
const boundary = resolve(stop);
|
package/src/lsp/service.mjs
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import { LspClient } from "./client.mjs";
|
|
2
2
|
import { LspDiagnosticStore } from "./diagnostic-store.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveLspServerStatus } from "./servers.mjs";
|
|
4
4
|
|
|
5
5
|
export class LspService {
|
|
6
|
-
constructor({ cwd }) {
|
|
6
|
+
constructor({ cwd, onEvent = null }) {
|
|
7
7
|
this.cwd = cwd;
|
|
8
|
+
this.onEvent = onEvent;
|
|
8
9
|
this.store = new LspDiagnosticStore();
|
|
9
10
|
this.clients = new Map();
|
|
10
11
|
this.spawning = new Map();
|
|
12
|
+
this.unavailable = new Map();
|
|
13
|
+
this.announced = new Set();
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
touchFile(path) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
17
|
+
const result = resolveLspServerStatus({ filePath: path, workspaceRoot: this.cwd });
|
|
18
|
+
if (result.status === "unsupported") return result;
|
|
19
|
+
if (result.status === "unavailable") {
|
|
20
|
+
this.unavailable.set(result.id, result);
|
|
21
|
+
this.#emitOnce(`unavailable:${result.id}:${result.reason}`, result);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const server = result.server;
|
|
16
26
|
const key = `${server.id}:${server.root}`;
|
|
17
27
|
const existing = this.clients.get(key);
|
|
18
28
|
if (existing) {
|
|
19
29
|
existing.touchFile(path);
|
|
20
|
-
return;
|
|
30
|
+
return { status: "already_attached", id: server.id, root: server.root };
|
|
21
31
|
}
|
|
22
32
|
if (this.spawning.has(key)) {
|
|
23
33
|
this.spawning.get(key).then((client) => client?.touchFile(path)).catch(() => {});
|
|
24
|
-
return;
|
|
34
|
+
return { status: "starting", id: server.id, root: server.root };
|
|
25
35
|
}
|
|
36
|
+
|
|
37
|
+
this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root });
|
|
26
38
|
const task = this.#startClient(server, key).then((client) => {
|
|
27
39
|
client?.touchFile(path);
|
|
28
40
|
return client;
|
|
@@ -31,13 +43,25 @@ export class LspService {
|
|
|
31
43
|
task.finally(() => {
|
|
32
44
|
if (this.spawning.get(key) === task) this.spawning.delete(key);
|
|
33
45
|
}).catch(() => {});
|
|
46
|
+
return { status: "starting", id: server.id, root: server.root };
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
snapshot() {
|
|
37
50
|
const diagnostics = this.store.snapshot();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
const servers = [
|
|
52
|
+
...[...this.clients.values()].map((client) => ({
|
|
53
|
+
id: client.serverId,
|
|
54
|
+
root: client.cwd,
|
|
55
|
+
status: client.status,
|
|
56
|
+
})),
|
|
57
|
+
...[...this.spawning.keys()].map((key) => ({
|
|
58
|
+
id: key.slice(0, key.indexOf(":")),
|
|
59
|
+
root: key.slice(key.indexOf(":") + 1),
|
|
60
|
+
status: "starting",
|
|
61
|
+
})),
|
|
62
|
+
...this.unavailable.values(),
|
|
63
|
+
];
|
|
64
|
+
return { status: summarizeStatus(servers), diagnostics, servers };
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
async dispose() {
|
|
@@ -56,10 +80,31 @@ export class LspService {
|
|
|
56
80
|
try {
|
|
57
81
|
await client.start();
|
|
58
82
|
this.clients.set(key, client);
|
|
83
|
+
this.unavailable.delete(server.id);
|
|
84
|
+
this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root });
|
|
59
85
|
return client;
|
|
60
|
-
} catch {
|
|
86
|
+
} catch (err) {
|
|
61
87
|
client.status = "failed";
|
|
88
|
+
const event = { status: "failed", id: server.id, root: server.root, reason: err.message };
|
|
89
|
+
this.unavailable.set(server.id, event);
|
|
90
|
+
this.#emitOnce(`failed:${key}:${err.message}`, event);
|
|
62
91
|
return null;
|
|
63
92
|
}
|
|
64
93
|
}
|
|
94
|
+
|
|
95
|
+
#emitOnce(key, event) {
|
|
96
|
+
if (this.announced.has(key)) return;
|
|
97
|
+
this.announced.add(key);
|
|
98
|
+
this.onEvent?.(event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function summarizeStatus(servers) {
|
|
103
|
+
const statuses = servers.map((server) => server.status);
|
|
104
|
+
if (statuses.includes("busy")) return "busy";
|
|
105
|
+
if (statuses.includes("starting")) return "starting";
|
|
106
|
+
if (statuses.includes("failed")) return "failed";
|
|
107
|
+
if (statuses.includes("ready") || statuses.includes("idle")) return "idle";
|
|
108
|
+
if (statuses.includes("unavailable")) return "unavailable";
|
|
109
|
+
return "";
|
|
65
110
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function formatLspServiceEvent(event) {
|
|
2
|
+
const id = event?.id ? String(event.id) : "server";
|
|
3
|
+
if (event?.status === "attached") return `LSP attached: ${id}`;
|
|
4
|
+
if (event?.status === "starting") return `LSP starting: ${id}`;
|
|
5
|
+
if (event?.status === "failed") return `LSP failed: ${id} - ${event.reason}`;
|
|
6
|
+
if (event?.status === "unavailable") return `LSP unavailable: ${id} - ${event.reason}`;
|
|
7
|
+
return `LSP ${event?.status ?? "status"}: ${id}`;
|
|
8
|
+
}
|