decorated-pi 0.4.0 → 0.5.0
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/README.md +74 -57
- package/extensions/guidance.ts +20 -13
- package/extensions/index.ts +72 -6
- package/extensions/io.ts +4 -3
- package/extensions/lsp/servers.ts +63 -3
- package/extensions/mcp/builtin.ts +348 -42
- package/extensions/mcp/client.ts +28 -19
- package/extensions/mcp/index.ts +407 -80
- package/extensions/model-integration.ts +19 -8
- package/extensions/patch.ts +232 -52
- package/extensions/rtk.ts +244 -0
- package/extensions/settings.ts +63 -1
- package/extensions/slash.ts +199 -67
- package/extensions/smart-at.ts +27 -1
- package/extensions/wakatime.ts +403 -0
- package/package.json +4 -5
package/extensions/settings.ts
CHANGED
|
@@ -26,8 +26,12 @@ export interface ProviderCache {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface McpServerEntry {
|
|
29
|
-
url
|
|
29
|
+
url?: string;
|
|
30
|
+
command?: string;
|
|
31
|
+
args?: string[];
|
|
32
|
+
env?: Record<string, string>;
|
|
30
33
|
enabled?: boolean;
|
|
34
|
+
description?: string;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface ModuleSettings {
|
|
@@ -36,11 +40,15 @@ export interface ModuleSettings {
|
|
|
36
40
|
"smart-at"?: boolean;
|
|
37
41
|
patch?: boolean;
|
|
38
42
|
mcp?: boolean;
|
|
43
|
+
wakatime?: boolean;
|
|
44
|
+
"rtk"?: boolean;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export interface DecoratedPiConfig {
|
|
42
48
|
imageModelKey?: string | null;
|
|
43
49
|
compactModelKey?: string | null;
|
|
50
|
+
mcpBrokerModelKey?: string | null;
|
|
51
|
+
mcpDescriptions?: Record<string, string>;
|
|
44
52
|
providers?: Record<string, ProviderCache>;
|
|
45
53
|
modules?: ModuleSettings;
|
|
46
54
|
mcpServers?: Record<string, McpServerEntry>;
|
|
@@ -93,6 +101,28 @@ export function removeProvider(name: string) {
|
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
// ─── Project-level config ────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function projectConfigPath(cwd: string): string {
|
|
107
|
+
return path.join(cwd, ".pi", "agent", "decorated-pi.json");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadProjectConfig(cwd: string): DecoratedPiConfig {
|
|
111
|
+
try {
|
|
112
|
+
const p = projectConfigPath(cwd);
|
|
113
|
+
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
114
|
+
} catch {}
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function saveProjectConfig(cwd: string, partial: Partial<DecoratedPiConfig>) {
|
|
119
|
+
const p = projectConfigPath(cwd);
|
|
120
|
+
const dir = path.dirname(p);
|
|
121
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
122
|
+
const current = loadProjectConfig(cwd);
|
|
123
|
+
fs.writeFileSync(p, JSON.stringify({ ...current, ...partial }, null, 2), "utf-8");
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
// ─── Getter ─────────────────────────────────────────────────────────────────
|
|
97
127
|
|
|
98
128
|
export function getImageModelKey(): string | null {
|
|
@@ -113,6 +143,36 @@ export function setCompactModelKey(key: string | null) {
|
|
|
113
143
|
saveConfig({ compactModelKey: key });
|
|
114
144
|
}
|
|
115
145
|
|
|
146
|
+
export function getMcpBrokerModelKey(): string | null {
|
|
147
|
+
return loadConfig().mcpBrokerModelKey ?? null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function setMcpBrokerModelKey(key: string | null) {
|
|
151
|
+
saveConfig({ mcpBrokerModelKey: key });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getMcpDescription(name: string, cwd?: string): string | undefined {
|
|
155
|
+
if (cwd) {
|
|
156
|
+
const projectCfg = loadProjectConfig(cwd);
|
|
157
|
+
if (projectCfg.mcpDescriptions?.[name]) return projectCfg.mcpDescriptions[name];
|
|
158
|
+
}
|
|
159
|
+
return loadConfig().mcpDescriptions?.[name];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function setMcpDescription(name: string, description: string, cwd?: string) {
|
|
163
|
+
if (cwd) {
|
|
164
|
+
const cfg = loadProjectConfig(cwd);
|
|
165
|
+
const descriptions = { ...cfg.mcpDescriptions };
|
|
166
|
+
descriptions[name] = description;
|
|
167
|
+
saveProjectConfig(cwd, { mcpDescriptions: descriptions });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const cfg = loadConfig();
|
|
171
|
+
const descriptions = { ...cfg.mcpDescriptions };
|
|
172
|
+
descriptions[name] = description;
|
|
173
|
+
saveConfig({ mcpDescriptions: descriptions });
|
|
174
|
+
}
|
|
175
|
+
|
|
116
176
|
// ─── Module Switches ──────────────────────────────────────────────────────────
|
|
117
177
|
|
|
118
178
|
const DEFAULT_MODULES: Required<ModuleSettings> = {
|
|
@@ -121,6 +181,8 @@ const DEFAULT_MODULES: Required<ModuleSettings> = {
|
|
|
121
181
|
"smart-at": true,
|
|
122
182
|
patch: true,
|
|
123
183
|
mcp: true,
|
|
184
|
+
wakatime: true,
|
|
185
|
+
"rtk": true,
|
|
124
186
|
};
|
|
125
187
|
|
|
126
188
|
export function isModuleEnabled(name: keyof ModuleSettings): boolean {
|
package/extensions/slash.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext, Theme as PiTheme } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { ModelPickerComponent } from "./model-integration.js";
|
|
11
11
|
import { getAllModuleSettings, setModuleEnabled, type ModuleSettings } from "./settings.js";
|
|
12
|
-
import { getMcpStatus } from "./mcp/index.js";
|
|
12
|
+
import { getMcpStatus, refreshServerCache, updateConfigEnabled } from "./mcp/index.js";
|
|
13
|
+
import { toggleMcpServerEnabled } from "./mcp/builtin.js";
|
|
13
14
|
import { Container, SettingsList, Spacer, Text, type TUI, type SettingsListTheme, type Component, getKeybindings } from "@earendil-works/pi-tui";
|
|
14
15
|
|
|
15
16
|
// ─── Border component (matches native DynamicBorder) ────────────────────────
|
|
@@ -44,7 +45,7 @@ function getSettingsListTheme(theme: PiTheme): SettingsListTheme {
|
|
|
44
45
|
|
|
45
46
|
function setupDpModelCommand(pi: ExtensionAPI) {
|
|
46
47
|
pi.registerCommand("dp-model", {
|
|
47
|
-
description: "Configure image and
|
|
48
|
+
description: "Configure image, compact, and MCP broker models",
|
|
48
49
|
handler: async (_args, ctx) => {
|
|
49
50
|
if (ctx.hasUI) {
|
|
50
51
|
await ctx.ui.custom<void>(
|
|
@@ -66,6 +67,8 @@ const MODULE_LABELS: Record<keyof ModuleSettings, string> = {
|
|
|
66
67
|
lsp: "LSP",
|
|
67
68
|
"smart-at": "@ overload",
|
|
68
69
|
mcp: "MCP",
|
|
70
|
+
wakatime: "WakaTime",
|
|
71
|
+
"rtk": "RTK",
|
|
69
72
|
};
|
|
70
73
|
|
|
71
74
|
const MODULE_DESCS: Record<keyof ModuleSettings, string> = {
|
|
@@ -74,6 +77,8 @@ const MODULE_DESCS: Record<keyof ModuleSettings, string> = {
|
|
|
74
77
|
lsp: "Language server diagnostics, hover, definition, references, symbols, rename",
|
|
75
78
|
"smart-at": "Project-aware file search replacing default autocomplete",
|
|
76
79
|
mcp: "MCP client for context7 and exa (zero-config)",
|
|
80
|
+
wakatime: "Send coding activity heartbeats to WakaTime",
|
|
81
|
+
"rtk": "Rewrite bash through system RTK when available",
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
class ModuleSettingsComponent extends Container {
|
|
@@ -133,109 +138,244 @@ function setupDpSettingsCommand(pi: ExtensionAPI) {
|
|
|
133
138
|
// ─── /mcp ──────────────────────────────────────────────────────────────────
|
|
134
139
|
|
|
135
140
|
class McpStatusComponent extends Container {
|
|
136
|
-
private
|
|
141
|
+
private linesComponent: Text;
|
|
142
|
+
private hintComponent: Text;
|
|
143
|
+
private notifyComponent: Text;
|
|
137
144
|
private tui: TUI;
|
|
138
145
|
private theme: PiTheme;
|
|
139
146
|
private done: () => void;
|
|
140
|
-
private
|
|
147
|
+
private registry: any;
|
|
148
|
+
private selected = 0;
|
|
149
|
+
private servers: ReturnType<typeof getMcpStatus> = [];
|
|
150
|
+
private notifyText = "";
|
|
151
|
+
private notifyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
152
|
+
private refreshing = new Set<string>();
|
|
153
|
+
private autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
141
154
|
|
|
142
|
-
|
|
155
|
+
private cwd: string;
|
|
156
|
+
|
|
157
|
+
constructor(tui: TUI, theme: PiTheme, registry: any, onDone: () => void, cwd: string) {
|
|
143
158
|
super();
|
|
144
159
|
this.tui = tui;
|
|
145
160
|
this.theme = theme;
|
|
146
161
|
this.done = onDone;
|
|
162
|
+
this.registry = registry;
|
|
163
|
+
this.cwd = cwd;
|
|
147
164
|
|
|
148
165
|
this.addChild(new DynamicBorder(theme));
|
|
149
166
|
this.addChild(new Spacer(1));
|
|
150
167
|
|
|
151
|
-
this.
|
|
152
|
-
this.addChild(this.
|
|
168
|
+
this.linesComponent = new Text("", 1, 0);
|
|
169
|
+
this.addChild(this.linesComponent);
|
|
153
170
|
|
|
154
171
|
this.addChild(new Spacer(1));
|
|
155
|
-
|
|
172
|
+
|
|
173
|
+
this.notifyComponent = new Text("", 1, 0);
|
|
174
|
+
this.addChild(this.notifyComponent);
|
|
175
|
+
|
|
176
|
+
this.hintComponent = new Text("", 1, 0);
|
|
177
|
+
this.addChild(this.hintComponent);
|
|
178
|
+
|
|
156
179
|
this.addChild(new Spacer(1));
|
|
157
180
|
this.addChild(new DynamicBorder(theme));
|
|
158
181
|
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
this.updateServers();
|
|
183
|
+
this.renderView();
|
|
184
|
+
|
|
185
|
+
// Auto-refresh while any server is still connecting
|
|
186
|
+
this.autoRefreshTimer = setInterval(() => {
|
|
187
|
+
this.updateServers();
|
|
188
|
+
this.renderView();
|
|
189
|
+
const allSettled = this.servers.every((s) => s.state !== "connecting");
|
|
190
|
+
if (allSettled && this.autoRefreshTimer) {
|
|
191
|
+
clearInterval(this.autoRefreshTimer);
|
|
192
|
+
this.autoRefreshTimer = null;
|
|
167
193
|
}
|
|
168
194
|
}, 500);
|
|
169
195
|
}
|
|
170
196
|
|
|
171
|
-
private
|
|
172
|
-
|
|
197
|
+
private updateServers() {
|
|
198
|
+
this.servers = getMcpStatus();
|
|
199
|
+
if (this.selected >= this.servers.length) {
|
|
200
|
+
this.selected = Math.max(0, this.servers.length - 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
173
203
|
|
|
174
|
-
|
|
175
|
-
|
|
204
|
+
private renderView() {
|
|
205
|
+
if (this.servers.length === 0) {
|
|
206
|
+
this.linesComponent.setText("No MCP servers configured.");
|
|
207
|
+
this.hintComponent.setText(this.theme.fg("dim", "q close"));
|
|
176
208
|
this.tui.requestRender();
|
|
177
209
|
return;
|
|
178
210
|
}
|
|
179
211
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
const lines: string[] = [`MCP servers (${this.servers.length}):`, ""];
|
|
213
|
+
|
|
214
|
+
const namePad = Math.max(...this.servers.map((s) => s.name.length), 12);
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < this.servers.length; i++) {
|
|
217
|
+
const s = this.servers[i];
|
|
218
|
+
const isSelected = i === this.selected;
|
|
219
|
+
const isDisabled = s.state === "disabled";
|
|
220
|
+
const cursor = isSelected ? this.theme.fg("accent", "→ ") : " ";
|
|
221
|
+
|
|
222
|
+
let statusIcon: string;
|
|
223
|
+
let statusColor: (s: string) => string;
|
|
224
|
+
if (s.state === "connected") {
|
|
225
|
+
statusIcon = "🟢";
|
|
226
|
+
statusColor = (str: string) => this.theme.fg("accent", str);
|
|
227
|
+
} else if (s.state === "connecting") {
|
|
228
|
+
statusIcon = "🟡";
|
|
229
|
+
statusColor = (str: string) => this.theme.fg("warning", str);
|
|
230
|
+
} else if (s.state === "disabled") {
|
|
231
|
+
statusIcon = "⚪";
|
|
232
|
+
statusColor = (str: string) => this.theme.fg("dim", str);
|
|
233
|
+
} else {
|
|
234
|
+
statusIcon = "🔴";
|
|
235
|
+
statusColor = (str: string) => this.theme.fg("error", str);
|
|
196
236
|
}
|
|
197
|
-
lines.push("");
|
|
198
|
-
}
|
|
199
237
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
238
|
+
const name = isDisabled
|
|
239
|
+
? this.theme.fg("dim", s.name)
|
|
240
|
+
: isSelected
|
|
241
|
+
? this.theme.fg("accent", s.name)
|
|
242
|
+
: s.name;
|
|
243
|
+
const namePadding = " ".repeat(Math.max(0, namePad - s.name.length));
|
|
244
|
+
const desc = s.description ? ` — ${s.description.slice(0, 50)}` : "";
|
|
245
|
+
const descDim = isDisabled ? this.theme.fg("dim", desc) : desc;
|
|
246
|
+
lines.push(
|
|
247
|
+
`${cursor}${name}${namePadding} ${statusIcon} ${statusColor(s.state)}${descDim}`
|
|
248
|
+
);
|
|
206
249
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
250
|
+
if (isSelected) {
|
|
251
|
+
lines.push(` ${this.theme.fg("dim", s.url)}`);
|
|
252
|
+
if (s.error) {
|
|
253
|
+
lines.push(` ${this.theme.fg("error", `Error: ${s.error}`)}`);
|
|
254
|
+
}
|
|
255
|
+
if (s.tools.length > 0) {
|
|
256
|
+
lines.push(` ${s.toolCount} tool${s.toolCount === 1 ? "" : "s"}:`);
|
|
257
|
+
for (const tool of s.tools.slice(0, 6)) {
|
|
258
|
+
const td = tool.description
|
|
259
|
+
? ` — ${tool.description.slice(0, 55)}`
|
|
260
|
+
: "";
|
|
261
|
+
lines.push(` ${tool.name}${td}`);
|
|
262
|
+
}
|
|
263
|
+
if (s.tools.length > 6) {
|
|
264
|
+
lines.push(` ... and ${s.tools.length - 6} more`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
lines.push("");
|
|
268
|
+
}
|
|
212
269
|
}
|
|
213
270
|
|
|
214
|
-
this.
|
|
271
|
+
this.linesComponent.setText(lines.join("\n"));
|
|
272
|
+
|
|
273
|
+
const s = this.servers[this.selected];
|
|
274
|
+
const toggleHint = s?.state === "disabled" ? "space enable" : "space disable";
|
|
275
|
+
const hintParts = ["↑↓ navigate", toggleHint, "r refresh", "q close"];
|
|
276
|
+
this.hintComponent.setText(this.theme.fg("dim", hintParts.join(" | ")));
|
|
277
|
+
this.notifyComponent.setText(
|
|
278
|
+
this.notifyText ? this.theme.fg("warning", this.notifyText) : ""
|
|
279
|
+
);
|
|
280
|
+
|
|
215
281
|
this.tui.requestRender();
|
|
216
282
|
}
|
|
217
283
|
|
|
284
|
+
private showNotify(text: string) {
|
|
285
|
+
this.notifyText = text;
|
|
286
|
+
this.renderView();
|
|
287
|
+
if (this.notifyTimer) clearTimeout(this.notifyTimer);
|
|
288
|
+
this.notifyTimer = setTimeout(() => {
|
|
289
|
+
this.notifyText = "";
|
|
290
|
+
this.renderView();
|
|
291
|
+
}, 3000);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private clearNotify() {
|
|
295
|
+
if (this.notifyTimer) {
|
|
296
|
+
clearTimeout(this.notifyTimer);
|
|
297
|
+
this.notifyTimer = null;
|
|
298
|
+
}
|
|
299
|
+
this.notifyText = "";
|
|
300
|
+
}
|
|
301
|
+
|
|
218
302
|
handleInput(data: string) {
|
|
219
303
|
const kb = getKeybindings();
|
|
304
|
+
|
|
305
|
+
// Navigation
|
|
306
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
307
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
308
|
+
this.clearNotify();
|
|
309
|
+
this.renderView();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
313
|
+
this.selected = Math.min(this.servers.length - 1, this.selected + 1);
|
|
314
|
+
this.clearNotify();
|
|
315
|
+
this.renderView();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Quit
|
|
220
320
|
if (
|
|
221
321
|
data === "q" ||
|
|
222
322
|
data === "\r" ||
|
|
223
323
|
data === "\n" ||
|
|
224
324
|
kb.matches(data, "tui.select.cancel")
|
|
225
325
|
) {
|
|
226
|
-
if (this.
|
|
227
|
-
|
|
228
|
-
this.timer = null;
|
|
229
|
-
}
|
|
326
|
+
if (this.autoRefreshTimer) { clearInterval(this.autoRefreshTimer); this.autoRefreshTimer = null; }
|
|
327
|
+
if (this.notifyTimer) { clearTimeout(this.notifyTimer); this.notifyTimer = null; }
|
|
230
328
|
this.done();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (this.servers.length === 0) return;
|
|
333
|
+
const s = this.servers[this.selected];
|
|
334
|
+
|
|
335
|
+
// Toggle enable/disable
|
|
336
|
+
if (data === " ") {
|
|
337
|
+
const scope = s.source === "project" ? "project" : "global";
|
|
338
|
+
const newEnabled = s.state === "disabled";
|
|
339
|
+
const ok = toggleMcpServerEnabled(s.name, newEnabled, scope, this.cwd || undefined);
|
|
340
|
+
if (ok) {
|
|
341
|
+
updateConfigEnabled(s.name, newEnabled);
|
|
342
|
+
}
|
|
343
|
+
this.showNotify(
|
|
344
|
+
ok
|
|
345
|
+
? `${newEnabled ? "Enabled" : "Disabled"} "${s.name}". Use /reload to apply.`
|
|
346
|
+
: `Failed to toggle "${s.name}".`
|
|
347
|
+
);
|
|
348
|
+
this.updateServers();
|
|
349
|
+
this.renderView();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Refresh cache (reconnect + update)
|
|
354
|
+
if (data === "r" || data === "r") {
|
|
355
|
+
if (this.refreshing.has(s.name)) return;
|
|
356
|
+
this.refreshing.add(s.name);
|
|
357
|
+
const targetName = s.name;
|
|
358
|
+
const targetIndex = this.selected;
|
|
359
|
+
this.showNotify(`Refreshing "${targetName}"...`);
|
|
360
|
+
(async () => {
|
|
361
|
+
const result = await refreshServerCache(targetName, this.registry);
|
|
362
|
+
this.refreshing.delete(targetName);
|
|
363
|
+
// Only update UI if user hasn't navigated away
|
|
364
|
+
if (this.selected === targetIndex) {
|
|
365
|
+
this.updateServers();
|
|
366
|
+
this.renderView();
|
|
367
|
+
this.showNotify(result.ok ? `Refreshed "${targetName}".` : `Refresh failed: ${result.error}`);
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
return;
|
|
231
371
|
}
|
|
372
|
+
|
|
373
|
+
|
|
232
374
|
}
|
|
233
375
|
|
|
234
376
|
dispose() {
|
|
235
|
-
if (this.
|
|
236
|
-
|
|
237
|
-
this.timer = null;
|
|
238
|
-
}
|
|
377
|
+
if (this.autoRefreshTimer) { clearInterval(this.autoRefreshTimer); this.autoRefreshTimer = null; }
|
|
378
|
+
if (this.notifyTimer) { clearTimeout(this.notifyTimer); this.notifyTimer = null; }
|
|
239
379
|
}
|
|
240
380
|
}
|
|
241
381
|
|
|
@@ -245,7 +385,7 @@ function setupMcpCommand(pi: ExtensionAPI) {
|
|
|
245
385
|
handler: async (_args, ctx) => {
|
|
246
386
|
if (ctx.hasUI) {
|
|
247
387
|
await ctx.ui.custom<void>(
|
|
248
|
-
(tui, theme, _kb, done) => new McpStatusComponent(tui, theme, () => done(undefined))
|
|
388
|
+
(tui, theme, _kb, done) => new McpStatusComponent(tui, theme, ctx.modelRegistry, () => done(undefined), ctx.cwd)
|
|
249
389
|
);
|
|
250
390
|
return;
|
|
251
391
|
}
|
|
@@ -283,7 +423,6 @@ function setupMcpCommand(pi: ExtensionAPI) {
|
|
|
283
423
|
// ─── /retry ────────────────────────────────────────────────────────────────
|
|
284
424
|
|
|
285
425
|
function setupRetryCommand(pi: ExtensionAPI) {
|
|
286
|
-
let shouldInjectRetryNote = false;
|
|
287
426
|
let retryInProgress = false;
|
|
288
427
|
|
|
289
428
|
pi.registerCommand("retry", {
|
|
@@ -296,7 +435,6 @@ function setupRetryCommand(pi: ExtensionAPI) {
|
|
|
296
435
|
if (!ctx.isIdle()) ctx.abort();
|
|
297
436
|
|
|
298
437
|
retryInProgress = true;
|
|
299
|
-
shouldInjectRetryNote = true;
|
|
300
438
|
pi.sendMessage(
|
|
301
439
|
{ customType: "retry-trigger", content: "Continue.", display: false },
|
|
302
440
|
{ triggerTurn: true }
|
|
@@ -304,12 +442,6 @@ function setupRetryCommand(pi: ExtensionAPI) {
|
|
|
304
442
|
},
|
|
305
443
|
});
|
|
306
444
|
|
|
307
|
-
pi.on("before_agent_start", async (event) => {
|
|
308
|
-
if (!shouldInjectRetryNote) return;
|
|
309
|
-
shouldInjectRetryNote = false;
|
|
310
|
-
return { systemPrompt: event.systemPrompt + "\n\nThe previous turn was interrupted by the system." };
|
|
311
|
-
});
|
|
312
|
-
|
|
313
445
|
pi.on("agent_start", () => { retryInProgress = false; });
|
|
314
446
|
}
|
|
315
447
|
|
package/extensions/smart-at.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { spawnSync } from "child_process";
|
|
|
34
34
|
import { existsSync } from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
37
|
+
import type { DependencyStatus } from "./rtk";
|
|
37
38
|
|
|
38
39
|
// ═══════════════════════════════════════════════════════════
|
|
39
40
|
// 类型
|
|
@@ -118,6 +119,31 @@ function computePenalty(filePath: string, isDir: boolean, gitIgnored: boolean):
|
|
|
118
119
|
|
|
119
120
|
const SPAWN_OPTS = { timeout: 5000, encoding: "utf-8" as const, maxBuffer: 10 * 1024 * 1024 };
|
|
120
121
|
|
|
122
|
+
function isGitWorkTree(cwd: string): boolean {
|
|
123
|
+
return spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { ...SPAWN_OPTS, cwd }).status === 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function commandExists(command: string): boolean {
|
|
127
|
+
const result = spawnSync(
|
|
128
|
+
process.platform === "win32" ? "where" : (process.env.SHELL || "sh"),
|
|
129
|
+
process.platform === "win32" ? [command] : ["-lc", `command -v '${command.replace(/'/g, `'"'"'`)}'`],
|
|
130
|
+
SPAWN_OPTS,
|
|
131
|
+
);
|
|
132
|
+
return result.status === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getSmartAtDependencyStatuses(cwd: string): DependencyStatus[] {
|
|
136
|
+
const isGit = isGitWorkTree(cwd);
|
|
137
|
+
return [{
|
|
138
|
+
module: "smart-at",
|
|
139
|
+
label: "fd",
|
|
140
|
+
state: commandExists("fd") ? "ok" : (isGit ? "n/a" : "missing"),
|
|
141
|
+
detail: isGit
|
|
142
|
+
? "Only needed outside Git repositories."
|
|
143
|
+
: "Install fd for non-Git project file discovery.",
|
|
144
|
+
}];
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
function collectCandidates(cwd: string): FileCandidate[] {
|
|
122
148
|
const candidates: FileCandidate[] = [];
|
|
123
149
|
|
|
@@ -133,7 +159,7 @@ function collectCandidates(cwd: string): FileCandidate[] {
|
|
|
133
159
|
const opts = { ...SPAWN_OPTS, cwd };
|
|
134
160
|
|
|
135
161
|
// ── 检测是否 git 仓库 ──
|
|
136
|
-
const isGit =
|
|
162
|
+
const isGit = isGitWorkTree(cwd);
|
|
137
163
|
|
|
138
164
|
if (isGit) {
|
|
139
165
|
collectGit(candidates, opts, isChildOfHardExclude);
|