feat(sproutclaw): modularize webui, extensions, and agent config layout
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
Restructure local extensions into per-feature directories, split WebUI into backend modules with slash commands and systemd support, and track prompts/skills under .pi/agent for portable Gitea deployment. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
6
packages/coding-agent/npm-shrinkwrap.json
generated
6
packages/coding-agent/npm-shrinkwrap.json
generated
@@ -1321,7 +1321,8 @@
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
@@ -1774,7 +1775,8 @@
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.2",
|
||||
|
||||
@@ -15,8 +15,15 @@ const MAX_DESCRIPTION_LENGTH = 1024;
|
||||
|
||||
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
|
||||
|
||||
/** Root-level markdown files that document the skills directory, not skills themselves. */
|
||||
const IGNORED_ROOT_SKILL_MARKDOWN = new Set(["README.md"]);
|
||||
|
||||
type IgnoreMatcher = ReturnType<typeof ignore>;
|
||||
|
||||
function isIgnoredRootSkillMarkdown(fileName: string): boolean {
|
||||
return IGNORED_ROOT_SKILL_MARKDOWN.has(fileName) || fileName.toLowerCase() === "readme.md";
|
||||
}
|
||||
|
||||
function toPosixPath(p: string): string {
|
||||
return p.split(sep).join("/");
|
||||
}
|
||||
@@ -259,7 +266,7 @@ function loadSkillsFromDirInternal(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isFile || !includeRootFiles || !entry.name.endsWith(".md")) {
|
||||
if (!isFile || !includeRootFiles || !entry.name.endsWith(".md") || isIgnoredRootSkillMarkdown(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -278,6 +285,10 @@ function loadSkillFromFile(
|
||||
filePath: string,
|
||||
source: string,
|
||||
): { skill: Skill | null; diagnostics: ResourceDiagnostic[] } {
|
||||
if (isIgnoredRootSkillMarkdown(basename(filePath))) {
|
||||
return { skill: null, diagnostics: [] };
|
||||
}
|
||||
|
||||
const diagnostics: ResourceDiagnostic[] = [];
|
||||
|
||||
try {
|
||||
|
||||
@@ -33,8 +33,6 @@ import {
|
||||
type Component,
|
||||
Container,
|
||||
fuzzyFilter,
|
||||
getCapabilities,
|
||||
hyperlink,
|
||||
Loader,
|
||||
type LoaderIndicatorOptions,
|
||||
Markdown,
|
||||
@@ -51,7 +49,6 @@ import { spawn, spawnSync } from "child_process";
|
||||
import {
|
||||
APP_NAME,
|
||||
APP_TITLE,
|
||||
getAgentDir,
|
||||
getAuthPath,
|
||||
getDebugLogPath,
|
||||
getDocsPath,
|
||||
@@ -75,7 +72,6 @@ import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/htt
|
||||
import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.ts";
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.ts";
|
||||
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts";
|
||||
import { DefaultPackageManager } from "../../core/package-manager.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.ts";
|
||||
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
|
||||
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
|
||||
@@ -92,7 +88,6 @@ import { getCwdRelativePath } from "../../utils/paths.ts";
|
||||
import { getPiUserAgent } from "../../utils/pi-user-agent.ts";
|
||||
import { killTrackedDetachedChildren } from "../../utils/shell.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.ts";
|
||||
import { ArminComponent } from "./components/armin.ts";
|
||||
import { AssistantMessageComponent } from "./components/assistant-message.ts";
|
||||
import { BashExecutionComponent } from "./components/bash-execution.ts";
|
||||
@@ -716,20 +711,6 @@ export class InteractiveMode {
|
||||
async run(): Promise<void> {
|
||||
await this.init();
|
||||
|
||||
// Start version check asynchronously
|
||||
checkForNewPiVersion(this.version).then((newRelease) => {
|
||||
if (newRelease) {
|
||||
this.showNewVersionNotification(newRelease);
|
||||
}
|
||||
});
|
||||
|
||||
// Start package update check asynchronously
|
||||
this.checkForPackageUpdates().then((updates) => {
|
||||
if (updates.length > 0) {
|
||||
this.showPackageUpdateNotification(updates);
|
||||
}
|
||||
});
|
||||
|
||||
// Check tmux keyboard setup asynchronously
|
||||
this.checkTmuxKeyboardSetup().then((warning) => {
|
||||
if (warning) {
|
||||
@@ -788,24 +769,6 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForPackageUpdates(): Promise<string[]> {
|
||||
if (process.env.PI_OFFLINE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const packageManager = new DefaultPackageManager({
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
agentDir: getAgentDir(),
|
||||
settingsManager: this.settingsManager,
|
||||
});
|
||||
const updates = await packageManager.checkForAvailableUpdates();
|
||||
return updates.map((update) => update.displayName);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async checkTmuxKeyboardSetup(): Promise<string | undefined> {
|
||||
if (!process.env.TMUX) return undefined;
|
||||
|
||||
@@ -1172,6 +1135,24 @@ export class InteractiveMode {
|
||||
);
|
||||
}
|
||||
|
||||
private splitExtensionsByOrigin(extensions: Array<{ path: string; sourceInfo?: SourceInfo }>): {
|
||||
local: Array<{ path: string; sourceInfo?: SourceInfo }>;
|
||||
packages: Array<{ path: string; sourceInfo?: SourceInfo }>;
|
||||
} {
|
||||
const local: Array<{ path: string; sourceInfo?: SourceInfo }> = [];
|
||||
const packages: Array<{ path: string; sourceInfo?: SourceInfo }> = [];
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (this.isPackageSource(extension.sourceInfo)) {
|
||||
packages.push(extension);
|
||||
} else {
|
||||
local.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
return { local, packages };
|
||||
}
|
||||
|
||||
private formatScopeGroups(
|
||||
groups: Array<{
|
||||
scope: "user" | "project" | "path";
|
||||
@@ -1294,23 +1275,58 @@ export class InteractiveMode {
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionHeader = (name: string, color: ThemeColor = "mdHeading") => theme.fg(color, `[${name}]`);
|
||||
const formatCompactList = (items: string[], options?: { sort?: boolean }): string => {
|
||||
const sectionHeader = (name: string, count?: number, color: ThemeColor = "mdHeading") => {
|
||||
const title = theme.fg(color, `[${name}]`);
|
||||
if (count === undefined) {
|
||||
return title;
|
||||
}
|
||||
return `${title} ${theme.fg("muted", String(count))}`;
|
||||
};
|
||||
const formatCompactGrid = (
|
||||
items: string[],
|
||||
options?: { sort?: boolean; columns?: number; indent?: number },
|
||||
): string => {
|
||||
const columns = Math.max(1, options?.columns ?? 4);
|
||||
const indent = " ".repeat(options?.indent ?? 2);
|
||||
const labels = items.map((item) => item.trim()).filter((item) => item.length > 0);
|
||||
if (options?.sort !== false) {
|
||||
labels.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
return theme.fg("dim", ` ${labels.join(", ")}`);
|
||||
if (labels.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const rows: string[][] = [];
|
||||
for (let index = 0; index < labels.length; index += columns) {
|
||||
rows.push(labels.slice(index, index + columns));
|
||||
}
|
||||
|
||||
const columnWidths = Array.from({ length: columns }, (_, columnIndex) =>
|
||||
Math.max(0, ...rows.map((row) => row[columnIndex]?.length ?? 0)),
|
||||
);
|
||||
|
||||
const body = rows
|
||||
.map((row) => {
|
||||
const cells = row.map((label, columnIndex) => {
|
||||
const width = columnWidths[columnIndex];
|
||||
return width > 0 ? label.padEnd(width + 2) : "";
|
||||
});
|
||||
return `${indent}${cells.join("").trimEnd()}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return theme.fg("dim", body);
|
||||
};
|
||||
const addLoadedSection = (
|
||||
name: string,
|
||||
collapsedBody: string,
|
||||
expandedBody = collapsedBody,
|
||||
color: ThemeColor = "mdHeading",
|
||||
count?: number,
|
||||
): void => {
|
||||
const section = new ExpandableText(
|
||||
() => `${sectionHeader(name, color)}\n${collapsedBody}`,
|
||||
() => `${sectionHeader(name, color)}\n${expandedBody}`,
|
||||
() => `${sectionHeader(name, count, color)}\n${collapsedBody}`,
|
||||
() => `${sectionHeader(name, count, color)}\n${expandedBody}`,
|
||||
this.getStartupExpansionState(),
|
||||
0,
|
||||
0,
|
||||
@@ -1357,11 +1373,11 @@ export class InteractiveMode {
|
||||
const contextList = contextFiles
|
||||
.map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
|
||||
.join("\n");
|
||||
const contextCompactList = formatCompactList(
|
||||
const contextCompactList = formatCompactGrid(
|
||||
contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)),
|
||||
{ sort: false },
|
||||
);
|
||||
addLoadedSection("Context", contextCompactList, contextList);
|
||||
addLoadedSection("系统设定", contextCompactList, contextList, "mdHeading", contextFiles.length);
|
||||
}
|
||||
|
||||
const skills = skillsResult.skills;
|
||||
@@ -1373,8 +1389,8 @@ export class InteractiveMode {
|
||||
formatPath: (item) => this.formatDisplayPath(item.path),
|
||||
formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
|
||||
});
|
||||
const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
|
||||
addLoadedSection("Skills", skillCompactList, skillList);
|
||||
const skillCompactList = formatCompactGrid(skills.map((skill) => skill.name));
|
||||
addLoadedSection("技能", skillCompactList, skillList, "mdHeading", skills.length);
|
||||
}
|
||||
|
||||
const templates = this.session.promptTemplates;
|
||||
@@ -1393,19 +1409,32 @@ export class InteractiveMode {
|
||||
return template ? `/${template.name}` : this.formatDisplayPath(item.path);
|
||||
},
|
||||
});
|
||||
const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
|
||||
addLoadedSection("Prompts", promptCompactList, templateList);
|
||||
const promptCompactList = formatCompactGrid(templates.map((template) => `/${template.name}`));
|
||||
addLoadedSection("指令", promptCompactList, templateList, "mdHeading", templates.length);
|
||||
}
|
||||
|
||||
if (extensions.length > 0) {
|
||||
const groups = this.buildScopeGroups(extensions);
|
||||
const extList = this.formatScopeGroups(groups, {
|
||||
formatPath: (item) => this.formatExtensionDisplayPath(item.path),
|
||||
formatPackagePath: (item) =>
|
||||
const extensionFormatOptions = {
|
||||
formatPath: (item: { path: string; sourceInfo?: SourceInfo }) =>
|
||||
this.formatExtensionDisplayPath(item.path),
|
||||
formatPackagePath: (item: { path: string; sourceInfo?: SourceInfo }) =>
|
||||
this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
|
||||
});
|
||||
const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
|
||||
addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
|
||||
};
|
||||
const { local, packages } = this.splitExtensionsByOrigin(extensions);
|
||||
|
||||
if (local.length > 0) {
|
||||
const localGroups = this.buildScopeGroups(local);
|
||||
const localList = this.formatScopeGroups(localGroups, extensionFormatOptions);
|
||||
const localCompactList = formatCompactGrid(this.getCompactExtensionLabels(local));
|
||||
addLoadedSection("本地扩展", localCompactList, localList, "mdHeading", local.length);
|
||||
}
|
||||
|
||||
if (packages.length > 0) {
|
||||
const packageGroups = this.buildScopeGroups(packages);
|
||||
const packageList = this.formatScopeGroups(packageGroups, extensionFormatOptions);
|
||||
const packageCompactList = formatCompactGrid(this.getCompactExtensionLabels(packages));
|
||||
addLoadedSection("npm 扩展", packageCompactList, packageList, "mdHeading", packages.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Show loaded themes (excluding built-in)
|
||||
@@ -1422,13 +1451,13 @@ export class InteractiveMode {
|
||||
formatPath: (item) => this.formatDisplayPath(item.path),
|
||||
formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
|
||||
});
|
||||
const themeCompactList = formatCompactList(
|
||||
const themeCompactList = formatCompactGrid(
|
||||
customThemes.map(
|
||||
(loadedTheme) =>
|
||||
loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath!, loadedTheme.sourceInfo),
|
||||
),
|
||||
);
|
||||
addLoadedSection("Themes", themeCompactList, themeList);
|
||||
addLoadedSection("Themes", themeCompactList, themeList, "mdHeading", customThemes.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3591,53 +3620,6 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showNewVersionNotification(release: LatestPiRelease): void {
|
||||
const action = theme.fg("accent", `${APP_NAME} update`);
|
||||
const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
|
||||
const changelogUrl = "https://pi.dev/changelog";
|
||||
const changelogLink = getCapabilities().hyperlinks
|
||||
? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
|
||||
: theme.fg("accent", changelogUrl);
|
||||
const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
|
||||
const note = release.note?.trim();
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.chatContainer.addChild(
|
||||
new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0),
|
||||
);
|
||||
if (note) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), {
|
||||
color: (text) => theme.fg("muted", text),
|
||||
}),
|
||||
);
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
this.chatContainer.addChild(new Text(changelogLine, 1, 0));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showPackageUpdateNotification(packages: string[]): void {
|
||||
const action = theme.fg("accent", `${APP_NAME} update`);
|
||||
const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
|
||||
const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.chatContainer.addChild(
|
||||
new Text(
|
||||
`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all queued messages (read-only).
|
||||
* Combines session queue and compaction queue.
|
||||
|
||||
3
packages/coding-agent/test/fixtures/skills/skills-dir-readme/README.md
vendored
Normal file
3
packages/coding-agent/test/fixtures/skills/skills-dir-readme/README.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Skills
|
||||
|
||||
Documentation for this skills directory. Not a skill file.
|
||||
6
packages/coding-agent/test/fixtures/skills/skills-dir-readme/nested-skill/SKILL.md
vendored
Normal file
6
packages/coding-agent/test/fixtures/skills/skills-dir-readme/nested-skill/SKILL.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: nested-skill
|
||||
description: Nested skill beside a README.
|
||||
---
|
||||
|
||||
# Nested Skill
|
||||
@@ -287,6 +287,8 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
) => (InteractiveMode as any).prototype.getCompactNonPackageExtensionLabel.call(fakeThis, p, index, allPaths),
|
||||
getCompactExtensionLabels: (extensions: ExtensionFixture[]) =>
|
||||
(InteractiveMode as any).prototype.getCompactExtensionLabels.call(fakeThis, extensions),
|
||||
splitExtensionsByOrigin: (extensions: ExtensionFixture[]) =>
|
||||
(InteractiveMode as any).prototype.splitExtensionsByOrigin.call(fakeThis, extensions),
|
||||
formatDiagnostics: () => "diagnostics",
|
||||
getBuiltInCommandConflictDiagnostics: () => [],
|
||||
};
|
||||
@@ -415,7 +417,7 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer);
|
||||
expect(output).toContain("[Skills]");
|
||||
expect(output).toContain("[技能]");
|
||||
expect(output).toContain("commit");
|
||||
expect(output).not.toContain("resource-list");
|
||||
});
|
||||
@@ -432,7 +434,7 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer);
|
||||
expect(output).toContain("[Skills]");
|
||||
expect(output).toContain("[技能]");
|
||||
expect(output).toContain("resource-list");
|
||||
expect(output).not.toContain("commit");
|
||||
});
|
||||
@@ -450,7 +452,7 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer);
|
||||
expect(output).toContain("[Skills]");
|
||||
expect(output).toContain("[技能]");
|
||||
expect(output).toContain("resource-list");
|
||||
expect(output).not.toContain("commit");
|
||||
});
|
||||
@@ -466,8 +468,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer);
|
||||
expect(output).toContain("[Extensions]");
|
||||
expect(output).toContain("answer.ts, btw.ts");
|
||||
expect(output).toContain("[本地扩展]");
|
||||
expect(output).toContain("answer.ts");
|
||||
expect(output).toContain("btw.ts");
|
||||
expect(output).not.toContain("extensions/answer.ts");
|
||||
});
|
||||
|
||||
@@ -483,8 +486,12 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
@scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`);
|
||||
"[本地扩展] 4
|
||||
answer.ts cli-extension.ts local-index user-index
|
||||
|
||||
[npm 扩展] 4
|
||||
@scope/pi-scoped HazAT/pi-interactive-subagents HazAT/pi-interactive-subagents:subagents pi-markdown-preview"
|
||||
`);
|
||||
});
|
||||
|
||||
test("adds more parent folders until local extension labels are unique", () => {
|
||||
@@ -529,8 +536,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
alpha/one, beta/one, gamma/one"`);
|
||||
"[本地扩展] 3
|
||||
alpha/one beta/one gamma/one"
|
||||
`);
|
||||
});
|
||||
|
||||
test("strips index.ts from local extension label, showing parent dir", () => {
|
||||
@@ -557,8 +565,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
plan-mode"`);
|
||||
"[本地扩展] 1
|
||||
plan-mode"
|
||||
`);
|
||||
});
|
||||
|
||||
test("strips index.js from local extension label, showing parent dir", () => {
|
||||
@@ -585,8 +594,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
plan-mode"`);
|
||||
"[本地扩展] 1
|
||||
plan-mode"
|
||||
`);
|
||||
});
|
||||
|
||||
test("mixed single-file and subdirectory index.ts extensions strip index.ts", () => {
|
||||
@@ -622,8 +632,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
plan-mode, webfetch.ts"`);
|
||||
"[本地扩展] 2
|
||||
plan-mode webfetch.ts"
|
||||
`);
|
||||
});
|
||||
|
||||
test("multiple index.ts with unique parent dirs need no disambiguation", () => {
|
||||
@@ -659,8 +670,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
bar, foo"`);
|
||||
"[本地扩展] 2
|
||||
bar foo"
|
||||
`);
|
||||
});
|
||||
|
||||
test("multiple index.ts with same parent dir name disambiguated with grandparent", () => {
|
||||
@@ -696,8 +708,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
alpha/tools, beta/tools"`);
|
||||
"[本地扩展] 2
|
||||
alpha/tools beta/tools"
|
||||
`);
|
||||
});
|
||||
|
||||
test("non-index file in subdirectory stays as filename", () => {
|
||||
@@ -724,8 +737,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
main.ts"`);
|
||||
"[本地扩展] 1
|
||||
main.ts"
|
||||
`);
|
||||
});
|
||||
|
||||
test("package extensions still strip index.ts correctly (regression guard)", () => {
|
||||
@@ -752,8 +766,9 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
pi-markdown-preview"`);
|
||||
"[npm 扩展] 1
|
||||
pi-markdown-preview"
|
||||
`);
|
||||
});
|
||||
test("captures mixed extension layouts in expanded output", () => {
|
||||
const fakeThis = createShowLoadedResourcesThis({
|
||||
@@ -768,21 +783,25 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
|
||||
"[Extensions]
|
||||
project
|
||||
/tmp/project/.pi/extensions/answer.ts
|
||||
/tmp/project/.pi/extensions/local-index
|
||||
git:github.com/HazAT/pi-interactive-subagents
|
||||
extensions
|
||||
extensions/subagents
|
||||
npm:@scope/pi-scoped
|
||||
extensions
|
||||
npm:pi-markdown-preview
|
||||
extensions
|
||||
user
|
||||
/tmp/agent/extensions/user-index
|
||||
path
|
||||
/tmp/temp/cli-extension.ts"`);
|
||||
"[本地扩展] 4
|
||||
project
|
||||
/tmp/project/.pi/extensions/answer.ts
|
||||
/tmp/project/.pi/extensions/local-index
|
||||
user
|
||||
/tmp/agent/extensions/user-index
|
||||
path
|
||||
/tmp/temp/cli-extension.ts
|
||||
|
||||
[npm 扩展] 4
|
||||
project
|
||||
git:github.com/HazAT/pi-interactive-subagents
|
||||
extensions
|
||||
extensions/subagents
|
||||
npm:@scope/pi-scoped
|
||||
extensions
|
||||
npm:pi-markdown-preview
|
||||
extensions"
|
||||
`);
|
||||
});
|
||||
|
||||
test("shows context paths relative to cwd while preserving full external paths", () => {
|
||||
@@ -799,8 +818,10 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/");
|
||||
expect(output).toContain("[Context]");
|
||||
expect(output).toContain("~/.pi/agent/AGENTS.md, AGENTS.md");
|
||||
expect(output).toContain("[系统设定]");
|
||||
expect(output).toContain("~/.pi/agent/AGENTS.md");
|
||||
expect(output).toContain("AGENTS.md");
|
||||
expect(output).not.toContain("~/.pi/agent/AGENTS.md, AGENTS.md");
|
||||
expect(output).not.toContain(`${cwd.replace(/\\/g, "/")}/AGENTS.md`);
|
||||
});
|
||||
|
||||
@@ -819,7 +840,7 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
});
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/");
|
||||
expect(output).toContain("[Context]");
|
||||
expect(output).toContain("[系统设定]");
|
||||
expect(output).toContain("~/.pi/agent/AGENTS.md");
|
||||
expect(output).toContain("~/Development/pi-mono/AGENTS.md");
|
||||
expect(output).not.toContain("~/.pi/agent/AGENTS.md, AGENTS.md");
|
||||
@@ -854,6 +875,6 @@ describe("InteractiveMode.showLoadedResources", () => {
|
||||
|
||||
const output = renderAll(fakeThis.chatContainer);
|
||||
expect(output).toContain("[Skill conflicts]");
|
||||
expect(output).not.toContain("[Skills]");
|
||||
expect(output).not.toContain("[技能]");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,6 +105,17 @@ describe("skills", () => {
|
||||
expect(diagnostics).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should ignore README.md at the skills directory root", () => {
|
||||
const { skills, diagnostics } = loadSkillsFromDir({
|
||||
dir: join(fixturesDir, "skills-dir-readme"),
|
||||
source: "test",
|
||||
});
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe("nested-skill");
|
||||
expect(diagnostics).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should prefer a directory's root SKILL.md over nested SKILL.md files", () => {
|
||||
const { skills, diagnostics } = loadSkillsFromDir({
|
||||
dir: join(fixturesDir, "root-skill-preferred"),
|
||||
|
||||
Reference in New Issue
Block a user