@ia-ccun/code-agent-cli 0.0.53-beta.1 → 0.0.53-beta.2
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/bin/cli.js +92 -68
- package/config/agent/extensions/aicode-advisor.ts +187 -64
- package/config/agent/extensions/aicode-banner.ts +101 -0
- package/config/agent/extensions/aicode-btw.ts +622 -180
- package/config/agent/extensions/aicode-cd.ts +246 -63
- package/config/agent/extensions/aicode-pets.ts +0 -11
- package/config/agent/extensions/aicode-plan.ts +2 -0
- package/config/agent/extensions/aicode-safety-guard.ts +3 -0
- package/config/agent/extensions/aicode-tips.ts +1 -16
- package/config/agent/extensions/context-monitor.ts +30 -15
- package/config/agent/extensions/context.ts +132 -60
- package/config/agent/extensions/custom-footer.ts +0 -5
- package/config/agent/extensions/custom.ts +41 -29
- package/config/agent/extensions/error-monitor.ts +37 -33
- package/config/agent/extensions/goal.ts +8 -6
- package/config/agent/extensions/msg-debug.ts +103 -112
- package/config/agent/extensions/review.ts +365 -164
- package/config/agent/extensions/working-msg.ts +2 -4
- package/dist/banner.d.ts.map +1 -1
- package/dist/banner.js +8 -1
- package/dist/banner.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +9 -2
- package/dist/config-loader.js.map +1 -1
- package/package.json +6 -7
- package/scripts/regression/results/.last_pid +1 -1
- package/scripts/regression/results/.last_workdir +1 -1
- package/scripts/regression/results/02-extension-loading.log +16 -27
- package/scripts/regression/results/03-prompt-stream.log +14 -23
- package/scripts/regression/results/03-rpc-protocol.log +20 -31
- package/scripts/regression/results/04-slash-commands-and-skills.log +11 -10
- package/scripts/regression/results/05-tool-execution.log +6 -4
- package/scripts/regression/results/06-pi-new-features.log +22 -26
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
import * as fs from "node:fs";
|
|
22
22
|
import * as os from "node:os";
|
|
23
23
|
import * as path from "node:path";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
SessionManager,
|
|
26
|
+
type ExtensionAPI,
|
|
27
|
+
type ExtensionCommandContext,
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
25
29
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
26
30
|
|
|
27
31
|
// ============== 类型定义 ==============
|
|
@@ -51,9 +55,7 @@ type CdSuggestion = {
|
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
// ============== 常量 ==============
|
|
54
|
-
|
|
55
58
|
const STORE_VERSION = 1;
|
|
56
|
-
const MAX_HISTORY_ENTRIES = 200;
|
|
57
59
|
const MAX_SUGGESTIONS = 24;
|
|
58
60
|
const MAX_CHILDREN_SCANNED = 300;
|
|
59
61
|
const STATUS_KEY = "cd-history";
|
|
@@ -75,12 +77,16 @@ const RESERVED_ALIAS_NAMES = new Set([
|
|
|
75
77
|
|
|
76
78
|
function getAgentDir(): string {
|
|
77
79
|
const configured = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
78
|
-
return configured
|
|
80
|
+
return configured
|
|
81
|
+
? path.resolve(expandTilde(configured))
|
|
82
|
+
: path.join(os.homedir(), ".aicode-cli", "agent");
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
function getStorePath(): string {
|
|
82
86
|
const configured = process.env.PI_CD_HISTORY_STORE_PATH?.trim();
|
|
83
|
-
return configured
|
|
87
|
+
return configured
|
|
88
|
+
? path.resolve(expandTilde(configured))
|
|
89
|
+
: path.join(getAgentDir(), "state", "cd-history.json");
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
function emptyStore(): CdStore {
|
|
@@ -113,7 +119,10 @@ function isDirectory(input: string): boolean {
|
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
function normalizeDirectory(
|
|
122
|
+
function normalizeDirectory(
|
|
123
|
+
input: string,
|
|
124
|
+
baseDir?: string,
|
|
125
|
+
): string | undefined {
|
|
117
126
|
const resolved = canonicalPath(input, baseDir);
|
|
118
127
|
return isDirectory(resolved) ? resolved : undefined;
|
|
119
128
|
}
|
|
@@ -142,9 +151,18 @@ function formatTimestamp(ts: number): string {
|
|
|
142
151
|
|
|
143
152
|
function parseTimestamp(input: string): number {
|
|
144
153
|
// 支持 "YYYY-MM-DD HH:mm:ss" 格式
|
|
145
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/.exec(
|
|
154
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/.exec(
|
|
155
|
+
input,
|
|
156
|
+
);
|
|
146
157
|
if (match) {
|
|
147
|
-
return new Date(
|
|
158
|
+
return new Date(
|
|
159
|
+
+match[1],
|
|
160
|
+
+match[2] - 1,
|
|
161
|
+
+match[3],
|
|
162
|
+
+match[4],
|
|
163
|
+
+match[5],
|
|
164
|
+
+match[6],
|
|
165
|
+
).getTime();
|
|
148
166
|
}
|
|
149
167
|
const fallback = Date.parse(input);
|
|
150
168
|
return Number.isFinite(fallback) ? fallback : Date.now();
|
|
@@ -176,7 +194,11 @@ function readStore(storePath: string): CdStore {
|
|
|
176
194
|
|
|
177
195
|
const store = emptyStore();
|
|
178
196
|
const rawAliases = (parsed as { aliases?: unknown }).aliases;
|
|
179
|
-
if (
|
|
197
|
+
if (
|
|
198
|
+
rawAliases &&
|
|
199
|
+
typeof rawAliases === "object" &&
|
|
200
|
+
!Array.isArray(rawAliases)
|
|
201
|
+
) {
|
|
180
202
|
for (const [name, target] of Object.entries(rawAliases)) {
|
|
181
203
|
if (!isValidAliasName(name) || typeof target !== "string") continue;
|
|
182
204
|
const normalized = normalizeDirectory(target);
|
|
@@ -188,14 +210,37 @@ function readStore(storePath: string): CdStore {
|
|
|
188
210
|
if (Array.isArray(rawHistory)) {
|
|
189
211
|
for (const item of rawHistory) {
|
|
190
212
|
if (!item || typeof item !== "object") continue;
|
|
191
|
-
const record = item as {
|
|
213
|
+
const record = item as {
|
|
214
|
+
path?: unknown;
|
|
215
|
+
uses?: unknown;
|
|
216
|
+
firstUsed?: unknown;
|
|
217
|
+
lastUsed?: unknown;
|
|
218
|
+
};
|
|
192
219
|
if (typeof record.path !== "string") continue;
|
|
193
220
|
const normalized = normalizeDirectory(record.path);
|
|
194
221
|
if (!normalized) continue;
|
|
195
|
-
const uses =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
const uses =
|
|
223
|
+
typeof record.uses === "number" && Number.isFinite(record.uses)
|
|
224
|
+
? Math.max(1, Math.floor(record.uses))
|
|
225
|
+
: 1;
|
|
226
|
+
const firstUsedRaw =
|
|
227
|
+
typeof record.firstUsed === "string"
|
|
228
|
+
? parseTimestamp(record.firstUsed)
|
|
229
|
+
: typeof record.firstUsed === "number"
|
|
230
|
+
? record.firstUsed
|
|
231
|
+
: Date.now();
|
|
232
|
+
const lastUsedRaw =
|
|
233
|
+
typeof record.lastUsed === "string"
|
|
234
|
+
? parseTimestamp(record.lastUsed)
|
|
235
|
+
: typeof record.lastUsed === "number"
|
|
236
|
+
? record.lastUsed
|
|
237
|
+
: firstUsedRaw;
|
|
238
|
+
store.history.push({
|
|
239
|
+
path: normalized,
|
|
240
|
+
uses,
|
|
241
|
+
firstUsed: formatTimestamp(firstUsedRaw),
|
|
242
|
+
lastUsed: formatTimestamp(lastUsedRaw),
|
|
243
|
+
});
|
|
199
244
|
}
|
|
200
245
|
}
|
|
201
246
|
|
|
@@ -210,7 +255,11 @@ function writeStore(storePath: string, store: CdStore): void {
|
|
|
210
255
|
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
211
256
|
const compacted = compactStore(store);
|
|
212
257
|
const tempPath = `${storePath}.${process.pid}.${Date.now()}.tmp`;
|
|
213
|
-
fs.writeFileSync(
|
|
258
|
+
fs.writeFileSync(
|
|
259
|
+
tempPath,
|
|
260
|
+
`${JSON.stringify(compacted, null, 2)}\n`,
|
|
261
|
+
"utf8",
|
|
262
|
+
);
|
|
214
263
|
fs.renameSync(tempPath, storePath);
|
|
215
264
|
} catch {
|
|
216
265
|
// Directory changes should still work if persistence fails.
|
|
@@ -244,8 +293,13 @@ function compactStore(store: CdStore): CdStore {
|
|
|
244
293
|
}
|
|
245
294
|
|
|
246
295
|
const history = Array.from(byPath.values())
|
|
247
|
-
|
|
248
|
-
.
|
|
296
|
+
// pi 1.1.0: lastUsed/firstUsed 是 ISO 字符串, 用 localeCompare 比较
|
|
297
|
+
.sort(
|
|
298
|
+
(a, b) =>
|
|
299
|
+
b.lastUsed.localeCompare(a.lastUsed) ||
|
|
300
|
+
b.uses - a.uses ||
|
|
301
|
+
a.path.localeCompare(b.path),
|
|
302
|
+
);
|
|
249
303
|
|
|
250
304
|
return { version: STORE_VERSION, aliases, history };
|
|
251
305
|
}
|
|
@@ -261,7 +315,12 @@ function recordVisit(store: CdStore, targetPath: string): CdStore {
|
|
|
261
315
|
existing.uses += 1;
|
|
262
316
|
existing.lastUsed = formatTimestamp(now);
|
|
263
317
|
} else {
|
|
264
|
-
next.history.unshift({
|
|
318
|
+
next.history.unshift({
|
|
319
|
+
path: normalized,
|
|
320
|
+
uses: 1,
|
|
321
|
+
firstUsed: formatTimestamp(now),
|
|
322
|
+
lastUsed: formatTimestamp(now),
|
|
323
|
+
});
|
|
265
324
|
}
|
|
266
325
|
return compactStore(next);
|
|
267
326
|
}
|
|
@@ -343,20 +402,35 @@ function matchScore(suggestion: CdSuggestion, query: string): number {
|
|
|
343
402
|
if (!trimmed) return suggestion.score;
|
|
344
403
|
|
|
345
404
|
const basename = path.basename(suggestion.targetPath).toLowerCase();
|
|
346
|
-
const fields = [
|
|
405
|
+
const fields = [
|
|
406
|
+
suggestion.value,
|
|
407
|
+
suggestion.label,
|
|
408
|
+
basename,
|
|
409
|
+
formatPath(suggestion.targetPath),
|
|
410
|
+
suggestion.targetPath,
|
|
411
|
+
].map((value) => value.toLowerCase());
|
|
347
412
|
|
|
348
413
|
if (fields.some((field) => field === trimmed)) return suggestion.score + 3000;
|
|
349
|
-
if (suggestion.value.toLowerCase().startsWith(trimmed))
|
|
414
|
+
if (suggestion.value.toLowerCase().startsWith(trimmed))
|
|
415
|
+
return suggestion.score + 2200;
|
|
350
416
|
if (basename.startsWith(trimmed)) return suggestion.score + 1800;
|
|
351
|
-
if (fields.some((field) => field.includes(trimmed)))
|
|
417
|
+
if (fields.some((field) => field.includes(trimmed)))
|
|
418
|
+
return suggestion.score + 1200;
|
|
352
419
|
|
|
353
|
-
const bestFuzzy = Math.max(
|
|
354
|
-
|
|
420
|
+
const bestFuzzy = Math.max(
|
|
421
|
+
...fields.map((field) => fuzzyScore(field, trimmed)),
|
|
422
|
+
);
|
|
423
|
+
return Number.isFinite(bestFuzzy)
|
|
424
|
+
? suggestion.score + bestFuzzy
|
|
425
|
+
: Number.NEGATIVE_INFINITY;
|
|
355
426
|
}
|
|
356
427
|
|
|
357
428
|
// ============== 建议生成 ==============
|
|
358
429
|
|
|
359
|
-
function addSuggestion(
|
|
430
|
+
function addSuggestion(
|
|
431
|
+
map: Map<string, CdSuggestion>,
|
|
432
|
+
suggestion: CdSuggestion,
|
|
433
|
+
): void {
|
|
360
434
|
const key = `${suggestion.source}:${suggestion.value}:${suggestion.targetPath}`;
|
|
361
435
|
const existing = map.get(key);
|
|
362
436
|
if (!existing || suggestion.score > existing.score) map.set(key, suggestion);
|
|
@@ -387,7 +461,11 @@ function readChildDirectories(cwd: string): CdSuggestion[] {
|
|
|
387
461
|
});
|
|
388
462
|
}
|
|
389
463
|
|
|
390
|
-
function buildSuggestions(
|
|
464
|
+
function buildSuggestions(
|
|
465
|
+
cwd: string,
|
|
466
|
+
store: CdStore,
|
|
467
|
+
query = "",
|
|
468
|
+
): CdSuggestion[] {
|
|
391
469
|
const suggestions = new Map<string, CdSuggestion>();
|
|
392
470
|
const normalizedCwd = canonicalPath(cwd);
|
|
393
471
|
const parent = path.dirname(normalizedCwd);
|
|
@@ -449,7 +527,11 @@ function buildSuggestions(cwd: string, store: CdStore, query = ""): CdSuggestion
|
|
|
449
527
|
return Array.from(suggestions.values())
|
|
450
528
|
.map((suggestion) => ({ suggestion, score: matchScore(suggestion, query) }))
|
|
451
529
|
.filter((entry) => Number.isFinite(entry.score))
|
|
452
|
-
.sort(
|
|
530
|
+
.sort(
|
|
531
|
+
(a, b) =>
|
|
532
|
+
b.score - a.score ||
|
|
533
|
+
a.suggestion.label.localeCompare(b.suggestion.label),
|
|
534
|
+
)
|
|
453
535
|
.slice(0, MAX_SUGGESTIONS)
|
|
454
536
|
.map((entry) => entry.suggestion);
|
|
455
537
|
}
|
|
@@ -459,7 +541,9 @@ function quoteArg(value: string): string {
|
|
|
459
541
|
return `"${value.replace(/["\\]/g, (match) => `\\${match}`)}"`;
|
|
460
542
|
}
|
|
461
543
|
|
|
462
|
-
function suggestionsToAutocompleteItems(
|
|
544
|
+
function suggestionsToAutocompleteItems(
|
|
545
|
+
suggestions: CdSuggestion[],
|
|
546
|
+
): AutocompleteItem[] {
|
|
463
547
|
return suggestions.map((suggestion) => ({
|
|
464
548
|
value: suggestion.value,
|
|
465
549
|
label: suggestion.label,
|
|
@@ -467,9 +551,14 @@ function suggestionsToAutocompleteItems(suggestions: CdSuggestion[]): Autocomple
|
|
|
467
551
|
}));
|
|
468
552
|
}
|
|
469
553
|
|
|
470
|
-
function resolveTarget(
|
|
554
|
+
function resolveTarget(
|
|
555
|
+
rawArgs: string,
|
|
556
|
+
cwd: string,
|
|
557
|
+
store: CdStore,
|
|
558
|
+
): { targetPath?: string; suggestions: CdSuggestion[] } {
|
|
471
559
|
const target = rawArgs.trim();
|
|
472
|
-
if (!target || target === ".")
|
|
560
|
+
if (!target || target === ".")
|
|
561
|
+
return { targetPath: canonicalPath(cwd), suggestions: [] };
|
|
473
562
|
|
|
474
563
|
// 别名匹配
|
|
475
564
|
const aliasTarget = store.aliases[target];
|
|
@@ -500,9 +589,15 @@ function resolveTarget(rawArgs: string, cwd: string, store: CdStore): { targetPa
|
|
|
500
589
|
function ensureEmptySessionFile(targetPath: string): string {
|
|
501
590
|
const sessionManager = SessionManager.create(targetPath);
|
|
502
591
|
const sessionPath = sessionManager.getSessionFile();
|
|
503
|
-
if (!sessionPath)
|
|
504
|
-
|
|
505
|
-
|
|
592
|
+
if (!sessionPath)
|
|
593
|
+
throw new Error(
|
|
594
|
+
"Unable to create a persistent aicode session for target directory",
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const internal = sessionManager as unknown as {
|
|
598
|
+
fileEntries?: unknown[];
|
|
599
|
+
_rewriteFile?: () => void;
|
|
600
|
+
};
|
|
506
601
|
if (typeof internal._rewriteFile === "function") {
|
|
507
602
|
internal._rewriteFile();
|
|
508
603
|
return sessionPath;
|
|
@@ -513,11 +608,18 @@ function ensureEmptySessionFile(targetPath: string): string {
|
|
|
513
608
|
throw new Error("Unable to initialize target aicode session header");
|
|
514
609
|
}
|
|
515
610
|
fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
|
|
516
|
-
fs.writeFileSync(
|
|
611
|
+
fs.writeFileSync(
|
|
612
|
+
sessionPath,
|
|
613
|
+
`${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
|
|
614
|
+
{ flag: "wx" },
|
|
615
|
+
);
|
|
517
616
|
return sessionPath;
|
|
518
617
|
}
|
|
519
618
|
|
|
520
|
-
function createTargetSession(
|
|
619
|
+
function createTargetSession(
|
|
620
|
+
currentSessionPath: string | undefined,
|
|
621
|
+
targetPath: string,
|
|
622
|
+
): string {
|
|
521
623
|
if (currentSessionPath && fs.existsSync(currentSessionPath)) {
|
|
522
624
|
try {
|
|
523
625
|
const forked = SessionManager.forkFrom(currentSessionPath, targetPath);
|
|
@@ -533,11 +635,22 @@ function createTargetSession(currentSessionPath: string | undefined, targetPath:
|
|
|
533
635
|
|
|
534
636
|
// ============== UI 交互 ==============
|
|
535
637
|
|
|
536
|
-
async function selectTarget(
|
|
638
|
+
async function selectTarget(
|
|
639
|
+
ctx: ExtensionCommandContext,
|
|
640
|
+
suggestions: CdSuggestion[],
|
|
641
|
+
title: string,
|
|
642
|
+
): Promise<CdSuggestion | undefined> {
|
|
537
643
|
if (!ctx.hasUI || suggestions.length === 0) return undefined;
|
|
538
644
|
|
|
539
645
|
const items = suggestions.map((suggestion, index) => {
|
|
540
|
-
const prefix =
|
|
646
|
+
const prefix =
|
|
647
|
+
suggestion.source === "alias"
|
|
648
|
+
? "★"
|
|
649
|
+
: suggestion.source === "history"
|
|
650
|
+
? "↺"
|
|
651
|
+
: suggestion.source === "special"
|
|
652
|
+
? "•"
|
|
653
|
+
: "dir";
|
|
541
654
|
return `${prefix} ${suggestion.label} — ${suggestion.description} [${index + 1}]`;
|
|
542
655
|
});
|
|
543
656
|
const selected = await ctx.ui.select(title, items);
|
|
@@ -565,7 +678,7 @@ function helpText(): string {
|
|
|
565
678
|
* 参考 Claude Code /cd 的行为:新目录的 CLAUDE.md / AGENTS.md /AICODE.md 作为消息注入
|
|
566
679
|
*/
|
|
567
680
|
function readProjectConfig(targetPath: string): string | null {
|
|
568
|
-
const candidates = ["CLAUDE.md", "AGENTS.md","AICODE.md"];
|
|
681
|
+
const candidates = ["CLAUDE.md", "AGENTS.md", "AICODE.md"];
|
|
569
682
|
const parts: string[] = [];
|
|
570
683
|
|
|
571
684
|
for (const name of candidates) {
|
|
@@ -611,7 +724,10 @@ async function changeDirectory(
|
|
|
611
724
|
const currentSessionPath = ctx.sessionManager.getSessionFile();
|
|
612
725
|
let targetSessionPath: string;
|
|
613
726
|
try {
|
|
614
|
-
targetSessionPath = createTargetSession(
|
|
727
|
+
targetSessionPath = createTargetSession(
|
|
728
|
+
currentSessionPath,
|
|
729
|
+
normalizedTarget,
|
|
730
|
+
);
|
|
615
731
|
} catch (error) {
|
|
616
732
|
const message = error instanceof Error ? error.message : String(error);
|
|
617
733
|
ctx.ui.notify(`cd: failed to create target session: ${message}`, "error");
|
|
@@ -648,11 +764,16 @@ async function changeDirectory(
|
|
|
648
764
|
|
|
649
765
|
function addAlias(store: CdStore, name: string, targetPath: string): CdStore {
|
|
650
766
|
if (!isValidAliasName(name)) {
|
|
651
|
-
throw new Error(
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Invalid alias '${name}'. Use letters, numbers, dot, underscore, or dash; start with a letter/number.`,
|
|
769
|
+
);
|
|
652
770
|
}
|
|
653
771
|
const normalized = normalizeDirectory(targetPath);
|
|
654
772
|
if (!normalized) throw new Error(`Not a directory: ${targetPath}`);
|
|
655
|
-
return compactStore({
|
|
773
|
+
return compactStore({
|
|
774
|
+
...store,
|
|
775
|
+
aliases: { ...store.aliases, [name]: normalized },
|
|
776
|
+
});
|
|
656
777
|
}
|
|
657
778
|
|
|
658
779
|
function removeAlias(store: CdStore, name: string): CdStore {
|
|
@@ -686,17 +807,33 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
686
807
|
|
|
687
808
|
// 注册 /cd 命令
|
|
688
809
|
pi.registerCommand("cd", {
|
|
689
|
-
description:
|
|
810
|
+
description:
|
|
811
|
+
"Change aicode-cli working directory with ranked history and aliases",
|
|
690
812
|
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
691
813
|
refreshStore();
|
|
692
814
|
const trimmed = prefix.trimStart();
|
|
693
815
|
const words = splitArgs(trimmed);
|
|
694
|
-
const commandOptions = [
|
|
816
|
+
const commandOptions = [
|
|
817
|
+
"--add",
|
|
818
|
+
"--remove",
|
|
819
|
+
"--list",
|
|
820
|
+
"--status",
|
|
821
|
+
"--clear-history",
|
|
822
|
+
"--help",
|
|
823
|
+
];
|
|
695
824
|
|
|
696
825
|
// 选项补全
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
826
|
+
if (
|
|
827
|
+
trimmed.startsWith("--") &&
|
|
828
|
+
words.length <= 1 &&
|
|
829
|
+
!trimmed.endsWith(" ")
|
|
830
|
+
) {
|
|
831
|
+
const filtered = commandOptions.filter((option) =>
|
|
832
|
+
option.startsWith(trimmed),
|
|
833
|
+
);
|
|
834
|
+
return filtered.length > 0
|
|
835
|
+
? filtered.map((option) => ({ value: option, label: option }))
|
|
836
|
+
: null;
|
|
700
837
|
}
|
|
701
838
|
|
|
702
839
|
// --remove 别名补全
|
|
@@ -705,24 +842,36 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
705
842
|
const partial = words[1] ?? "";
|
|
706
843
|
const items = Object.entries(store.aliases)
|
|
707
844
|
.filter(([name]) => name.startsWith(partial))
|
|
708
|
-
.map(([name, target]) => ({
|
|
845
|
+
.map(([name, target]) => ({
|
|
846
|
+
value: `${option} ${name}`,
|
|
847
|
+
label: name,
|
|
848
|
+
description: formatPath(target),
|
|
849
|
+
}));
|
|
709
850
|
return items.length > 0 ? items : null;
|
|
710
851
|
}
|
|
711
852
|
|
|
712
853
|
// --add 路径补全
|
|
713
|
-
if (
|
|
854
|
+
if (
|
|
855
|
+
words[0] === "--add" &&
|
|
856
|
+
words[1] &&
|
|
857
|
+
(trimmed.endsWith(" ") || words.length >= 3)
|
|
858
|
+
) {
|
|
714
859
|
const aliasName = words[1];
|
|
715
860
|
const pathPrefix = words.length >= 3 ? words.slice(2).join(" ") : "";
|
|
716
|
-
const items = buildSuggestions(currentCwd, store, pathPrefix).map(
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
861
|
+
const items = buildSuggestions(currentCwd, store, pathPrefix).map(
|
|
862
|
+
(suggestion) => ({
|
|
863
|
+
value: `--add ${aliasName} ${quoteArg(formatPath(suggestion.targetPath))}`,
|
|
864
|
+
label: suggestion.label,
|
|
865
|
+
description: suggestion.description,
|
|
866
|
+
}),
|
|
867
|
+
);
|
|
721
868
|
return items.length > 0 ? items : null;
|
|
722
869
|
}
|
|
723
870
|
|
|
724
871
|
// 普通路径补全
|
|
725
|
-
const items = suggestionsToAutocompleteItems(
|
|
872
|
+
const items = suggestionsToAutocompleteItems(
|
|
873
|
+
buildSuggestions(currentCwd, store, prefix),
|
|
874
|
+
);
|
|
726
875
|
return items.length > 0 ? items : null;
|
|
727
876
|
},
|
|
728
877
|
handler: async (args, ctx) => {
|
|
@@ -747,7 +896,9 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
747
896
|
|
|
748
897
|
// 清除历史
|
|
749
898
|
if (trimmed === "--clear-history") {
|
|
750
|
-
const ok =
|
|
899
|
+
const ok =
|
|
900
|
+
!ctx.hasUI ||
|
|
901
|
+
(await ctx.ui.confirm("Clear /cd history?", "Aliases will be kept."));
|
|
751
902
|
if (!ok) return;
|
|
752
903
|
setStore({ ...store, history: [] });
|
|
753
904
|
ctx.ui.notify("/cd history cleared", "info");
|
|
@@ -757,8 +908,18 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
757
908
|
// 列表选择
|
|
758
909
|
if (trimmed === "--list") {
|
|
759
910
|
const suggestions = buildSuggestions(currentCwd, store);
|
|
760
|
-
const selected = await selectTarget(
|
|
761
|
-
|
|
911
|
+
const selected = await selectTarget(
|
|
912
|
+
ctx,
|
|
913
|
+
suggestions,
|
|
914
|
+
"/cd aliases and history",
|
|
915
|
+
);
|
|
916
|
+
if (selected)
|
|
917
|
+
await changeDirectory(
|
|
918
|
+
ctx,
|
|
919
|
+
selected.targetPath,
|
|
920
|
+
() => store,
|
|
921
|
+
setStore,
|
|
922
|
+
);
|
|
762
923
|
return;
|
|
763
924
|
}
|
|
764
925
|
|
|
@@ -771,12 +932,17 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
771
932
|
ctx.ui.notify("Usage: /cd --add <name> [dir]", "error");
|
|
772
933
|
return;
|
|
773
934
|
}
|
|
774
|
-
const rawTarget =
|
|
935
|
+
const rawTarget =
|
|
936
|
+
words.length >= 3 ? words.slice(2).join(" ") : currentCwd;
|
|
775
937
|
try {
|
|
776
938
|
setStore(addAlias(store, name, rawTarget));
|
|
777
|
-
ctx.ui.notify(
|
|
939
|
+
ctx.ui.notify(
|
|
940
|
+
`Added /cd ${name} → ${formatPath(store.aliases[name] ?? normalizeDirectory(rawTarget, currentCwd) ?? rawTarget)}`,
|
|
941
|
+
"info",
|
|
942
|
+
);
|
|
778
943
|
} catch (error) {
|
|
779
|
-
const message =
|
|
944
|
+
const message =
|
|
945
|
+
error instanceof Error ? error.message : String(error);
|
|
780
946
|
ctx.ui.notify(`cd alias failed: ${message}`, "error");
|
|
781
947
|
}
|
|
782
948
|
return;
|
|
@@ -800,8 +966,18 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
800
966
|
|
|
801
967
|
// 无参数:显示选择列表
|
|
802
968
|
if (!trimmed) {
|
|
803
|
-
const selected = await selectTarget(
|
|
804
|
-
|
|
969
|
+
const selected = await selectTarget(
|
|
970
|
+
ctx,
|
|
971
|
+
buildSuggestions(currentCwd, store),
|
|
972
|
+
"Change directory",
|
|
973
|
+
);
|
|
974
|
+
if (selected)
|
|
975
|
+
await changeDirectory(
|
|
976
|
+
ctx,
|
|
977
|
+
selected.targetPath,
|
|
978
|
+
() => store,
|
|
979
|
+
setStore,
|
|
980
|
+
);
|
|
805
981
|
return;
|
|
806
982
|
}
|
|
807
983
|
|
|
@@ -813,7 +989,11 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
813
989
|
}
|
|
814
990
|
|
|
815
991
|
// 没有精确匹配:显示建议
|
|
816
|
-
const selected = await selectTarget(
|
|
992
|
+
const selected = await selectTarget(
|
|
993
|
+
ctx,
|
|
994
|
+
resolved.suggestions,
|
|
995
|
+
`No exact directory for '${trimmed}'. Pick one:`,
|
|
996
|
+
);
|
|
817
997
|
if (selected) {
|
|
818
998
|
await changeDirectory(ctx, selected.targetPath, () => store, setStore);
|
|
819
999
|
return;
|
|
@@ -828,7 +1008,10 @@ export default function cdHistoryExtension(pi: ExtensionAPI) {
|
|
|
828
1008
|
description: "Reload /cd history and alias store",
|
|
829
1009
|
handler: async (_args, ctx) => {
|
|
830
1010
|
refreshStore();
|
|
831
|
-
ctx.ui.notify(
|
|
1011
|
+
ctx.ui.notify(
|
|
1012
|
+
`Reloaded /cd store (${Object.keys(store.aliases).length} aliases, ${store.history.length} history entries)`,
|
|
1013
|
+
"info",
|
|
1014
|
+
);
|
|
832
1015
|
},
|
|
833
1016
|
});
|
|
834
1017
|
}
|
|
@@ -266,16 +266,6 @@ function getNextFrame(): string[] {
|
|
|
266
266
|
|
|
267
267
|
return frame;
|
|
268
268
|
}
|
|
269
|
-
|
|
270
|
-
function showPetNotification(ctx: ExtensionAPI["on"] extends (event: string, cb: (event: any, ctx: infer Ctx) => void) => void ? Ctx : never) {
|
|
271
|
-
const pet = PETS[currentPet]!;
|
|
272
|
-
const hatNames = getHatNames();
|
|
273
|
-
let msg = `${pet.emoji} 本次宠物:${pet.name}`;
|
|
274
|
-
if (isShiny) msg += " ✨(闪亮)";
|
|
275
|
-
if (currentHat !== "none") msg += ` + ${hatNames[currentHat]}`;
|
|
276
|
-
ctx.ui.notify(msg, "info");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
269
|
function startPetAnimation(ctx: any) {
|
|
280
270
|
const pet = PETS[currentPet]!;
|
|
281
271
|
|
|
@@ -305,7 +295,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
305
295
|
// 注册 /pet 命令
|
|
306
296
|
pi.registerCommand("pet", {
|
|
307
297
|
description: "切换随机宠物",
|
|
308
|
-
usage: "/pet",
|
|
309
298
|
handler: async (_args, ctx) => {
|
|
310
299
|
// 清理旧动画
|
|
311
300
|
if (intervalId) {
|
|
@@ -463,6 +463,7 @@ export default function safetyGuard(pi: ExtensionAPI) {
|
|
|
463
463
|
return;
|
|
464
464
|
}
|
|
465
465
|
if (ctx.hasUI) ctx.ui.notify("Usage: /safety-guard on|off|status|allow-list|allow-clear-session|allow-clear-permanent", "warning");
|
|
466
|
+
return;
|
|
466
467
|
},
|
|
467
468
|
});
|
|
468
469
|
|
|
@@ -548,5 +549,7 @@ export default function safetyGuard(pi: ExtensionAPI) {
|
|
|
548
549
|
if (decision && "block" in decision) return decision;
|
|
549
550
|
return;
|
|
550
551
|
}
|
|
552
|
+
// 其他工具不拦截
|
|
553
|
+
return;
|
|
551
554
|
});
|
|
552
555
|
}
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* 技巧列表包含快捷键、命令使用及联系方式提示
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { ExtensionAPI
|
|
10
|
-
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
10
|
const AICODE_TIPS = [
|
|
12
11
|
"@ 快速搜索项目文件",
|
|
13
12
|
"Ctrl+L 快速切换备选模型",
|
|
@@ -184,12 +183,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
183
|
const b = Math.max(0, Math.min(255, baseB + Math.floor(Math.random() * offset * 2 - offset)));
|
|
185
184
|
return `\x1b[38;2;${r};${g};${b}m`;
|
|
186
185
|
};
|
|
187
|
-
|
|
188
|
-
const hideTip = () => {
|
|
189
|
-
if (!currentUI) return;
|
|
190
|
-
currentUI.setWidget("aicode-tips", [], { placement: "aboveEditor" });
|
|
191
|
-
};
|
|
192
|
-
|
|
193
186
|
const clearAllTimers = () => {
|
|
194
187
|
if (autoHideTimeoutId) {
|
|
195
188
|
clearTimeout(autoHideTimeoutId);
|
|
@@ -201,14 +194,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
201
194
|
}
|
|
202
195
|
};
|
|
203
196
|
|
|
204
|
-
const padRight = (str: string, width: number): string => {
|
|
205
|
-
const chinese = (str.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
206
|
-
const ascii = str.length - chinese;
|
|
207
|
-
const fullWidth = chinese * 2 + ascii;
|
|
208
|
-
const padding = Math.max(0, width - fullWidth);
|
|
209
|
-
return str + " ".repeat(padding);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
197
|
const showRandomTip = (isInternalCall = false) => {
|
|
213
198
|
if (!currentUI) return;
|
|
214
199
|
|