Skip to content
6 changes: 3 additions & 3 deletions extensions/apply-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,10 +438,10 @@ export default function applyPatchExtension(pi: ExtensionAPI): void {
pi.registerTool({
name: "apply_patch",
label: "apply_patch",
description: "Apply a Codex-style patch to files. Supports add, update, delete, and move operations; absolute paths are allowed and writes run with the extension process' filesystem permissions.",
promptSnippet: "Apply Codex-style patches to files with add/update/delete/move operations",
description: "Apply a patch across one or more files in a single call. Handles add, update, delete, and move across multiple files; absolute paths allowed. Best for cross-file changes where individual edits would be tedious.",
promptSnippet: "Apply patches across multiple files with add/update/delete/move in one call",
promptGuidelines: [
"Use apply_patch for precise multi-file edits when exact text replacement is too awkward.",
"Use apply_patch when a change spans multiple files — all hunks apply in one tool call instead of N separate edits.",
"Patch text must start with *** Begin Patch and end with *** End Patch.",
"This tool can write absolute paths and delete or move files; inspect paths carefully before using it.",
"This tool is a direct extension-process filesystem writer, not the built-in bash approval flow.",
Expand Down
53 changes: 52 additions & 1 deletion extensions/capy-tools-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,42 @@ export type CodexFastConfig = {
enabled: boolean;
};

export const ALL_TOOL_IDS = [
"fetch",
"enable-builtin-search",
"repo-map",
"read-block",
"symbol-outline",
"apply-patch",
"terminal-session",
"ask-user",
"ask-question",
"ask-questionnaire",
"sourcegraph",
"recap",
"message-shape-diagnostic",
"auto-compact",
"codex-fast",
"capy-tools-settings",
"command-history",
"efforts",
"codex-goal",
"rtk",
"thinking-steps",
"todo",
"showsignature",
"working-message",
] as const;

export type ToolId = (typeof ALL_TOOL_IDS)[number];

export type ToolsConfig = Record<ToolId, boolean>;

export type CapyToolsSettings = {
workingMessage: WorkingMessageSettings;
autoCompact: AutoCompactConfig;
codexFast: CodexFastConfig;
tools: ToolsConfig;
};

export const DEFAULT_WORKING_MESSAGE_SETTINGS: WorkingMessageSettings = {
Expand All @@ -68,10 +100,15 @@ export const DEFAULT_CODEX_FAST_CONFIG: CodexFastConfig = {
enabled: false,
};

export const DEFAULT_TOOLS_CONFIG: ToolsConfig = Object.fromEntries(
ALL_TOOL_IDS.map((id) => [id, true]),
) as ToolsConfig;

export const DEFAULT_CAPY_TOOLS_SETTINGS: CapyToolsSettings = {
workingMessage: DEFAULT_WORKING_MESSAGE_SETTINGS,
autoCompact: DEFAULT_AUTO_COMPACT_CONFIG,
codexFast: DEFAULT_CODEX_FAST_CONFIG,
tools: { ...DEFAULT_TOOLS_CONFIG },
};

export const AUTO_COMPACT_PRESETS = [80, 85, 90, 95] as const;
Expand Down Expand Up @@ -140,15 +177,29 @@ export function normalizeCodexFastConfig(value: unknown): CodexFastConfig {
};
}

export function normalizeToolsConfig(value: unknown): ToolsConfig {
const defaults = { ...DEFAULT_TOOLS_CONFIG };
if (!value || typeof value !== "object") return defaults;

const raw = value as Record<string, unknown>;
for (const id of ALL_TOOL_IDS) {
if (typeof raw[id] === "boolean") {
(defaults as Record<string, boolean>)[id] = raw[id] as boolean;
}
}
return defaults;
}

export function normalizeCapyToolsSettings(value: unknown): CapyToolsSettings {
if (!value || typeof value !== "object") return structuredClone(DEFAULT_CAPY_TOOLS_SETTINGS);

const raw = value as { workingMessage?: unknown; autoCompact?: unknown; codexFast?: unknown };
const raw = value as { workingMessage?: unknown; autoCompact?: unknown; codexFast?: unknown; tools?: unknown };
return {
// Legacy cat-whimsical config stored `language` at the top level.
workingMessage: normalizeWorkingMessageSettings(raw.workingMessage ?? value),
autoCompact: normalizeAutoCompactConfig(raw.autoCompact),
codexFast: normalizeCodexFastConfig(raw.codexFast),
tools: normalizeToolsConfig(raw.tools),
};
}

Expand Down
72 changes: 71 additions & 1 deletion extensions/capy-tools-settings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";

import {
ALL_TOOL_IDS,
AUTO_COMPACT_PRESETS,
DEFAULT_AUTO_COMPACT_CONFIG,
KEEP_RECENT_PRESETS,
Expand All @@ -17,12 +18,14 @@ import { formatCodexFastStatus, setCodexFastEnabled } from "./codex-fast.ts";

function formatSettingsSummary(): string {
const settings = getCapyToolsSettings();
const enabledCount = Object.values(settings.tools).filter(Boolean).length;
return [
`Working message language: ${loadLanguageLabel(settings.workingMessage.language)}`,
`Auto-compact threshold: ${settings.autoCompact.autoCompactPercent}%`,
`Keep recent budget: ${settings.autoCompact.keepRecentPercent}%`,
`Strategy: ${STRATEGY_LABELS[settings.autoCompact.strategy]}`,
`Codex fast mode: ${settings.codexFast.enabled ? "enabled" : "disabled"}`,
`Tools enabled: ${enabledCount}/${ALL_TOOL_IDS.length}`,
].join("\n");
}

Expand All @@ -38,6 +41,36 @@ async function setWorkingMessageLanguage(ctx: ExtensionContext, languageText: st
return true;
}

async function openToolsMenu(ctx: ExtensionContext): Promise<void> {
while (true) {
const settings = getCapyToolsSettings();
const lines: string[] = [];
for (const id of ALL_TOOL_IDS) {
const icon = settings.tools[id] ? "✅" : "❌";
lines.push(`${icon} ${id}`);
}

const choice = await ctx.ui.select(
`Tools\n\n${lines.join("\n")}\n\nSelect a tool to toggle, or Done.`,
[...ALL_TOOL_IDS.map((id) => `${settings.tools[id] ? "Disable" : "Enable"} ${id}`), "Done"],
);

if (!choice || choice === "Done") return;

const match = choice.match(/^(Enable|Disable) (.+)$/);
if (!match) continue;

const toolId = match[2];
const enable = match[1] === "Enable";

await updateCapyToolsSettings((s) => ({
...s,
tools: { ...s.tools, [toolId]: enable },
}));
ctx.ui.notify(`Tool "${toolId}" ${enable ? "enabled" : "disabled"}. Restart pi or /reload for changes.`, "info");
}
}

async function openSettingsMenu(ctx: ExtensionContext): Promise<void> {
await restoreCapyToolsSettings();

Expand All @@ -51,6 +84,7 @@ async function openSettingsMenu(ctx: ExtensionContext): Promise<void> {
`Keep recent budget [${settings.autoCompact.keepRecentPercent}%]`,
`Compaction strategy [${settings.autoCompact.strategy}]`,
`Codex fast mode [${settings.codexFast.enabled ? "enabled" : "disabled"}]`,
"Tools (enable/disable individual tools)",
"Auto-compact status",
"Codex fast status",
"Reset auto-compact defaults",
Expand Down Expand Up @@ -123,6 +157,11 @@ async function openSettingsMenu(ctx: ExtensionContext): Promise<void> {
continue;
}

if (choice.startsWith("Tools")) {
await openToolsMenu(ctx);
continue;
}

if (choice === "Auto-compact status") {
ctx.ui.notify(formatAutoCompactStatus(ctx), "info");
continue;
Expand Down Expand Up @@ -158,6 +197,8 @@ export default function capyToolsSettingsExtension(pi: ExtensionAPI): void {
"codex-fast off",
"codex-fast toggle",
"codex-fast status",
"tools",
...ALL_TOOL_IDS.flatMap((id) => [`enable ${id}`, `disable ${id}`]),
"en",
"zh",
"ja",
Expand Down Expand Up @@ -203,6 +244,35 @@ export default function capyToolsSettingsExtension(pi: ExtensionAPI): void {
return;
}

if (trimmed === "tools") {
await openToolsMenu(ctx);
return;
}

const enableMatch = trimmed.match(/^enable (.+)$/i);
if (enableMatch) {
const toolId = enableMatch[1].trim();
if (!ALL_TOOL_IDS.includes(toolId as typeof ALL_TOOL_IDS[number])) {
ctx.ui.notify(`Unknown tool: ${toolId}`, "warning");
return;
}
await updateCapyToolsSettings((s) => ({ ...s, tools: { ...s.tools, [toolId]: true } }));
ctx.ui.notify(`Tool "${toolId}" enabled. Restart pi or /reload for changes.`, "info");
return;
}

const disableMatch = trimmed.match(/^disable (.+)$/i);
if (disableMatch) {
const toolId = disableMatch[1].trim();
if (!ALL_TOOL_IDS.includes(toolId as typeof ALL_TOOL_IDS[number])) {
ctx.ui.notify(`Unknown tool: ${toolId}`, "warning");
return;
}
await updateCapyToolsSettings((s) => ({ ...s, tools: { ...s.tools, [toolId]: false } }));
ctx.ui.notify(`Tool "${toolId}" disabled. Restart pi or /reload for changes.`, "info");
return;
}

if (trimmed === "reset-auto-compact" || trimmed === "auto-compact reset") {
await persistAutoCompactConfig({ ...DEFAULT_AUTO_COMPACT_CONFIG });
ctx.ui.notify("Auto-compact settings reset to defaults.", "info");
Expand All @@ -212,7 +282,7 @@ export default function capyToolsSettingsExtension(pi: ExtensionAPI): void {
if (await setWorkingMessageLanguage(ctx, trimmed)) return;

ctx.ui.notify(
"Usage: /capy-tools-settings [settings|status|reset-auto-compact|codex-fast on|codex-fast off|en|zh|ja|ko]",
"Usage: /capy-tools-settings [settings|status|tools|enable <tool>|disable <tool>|codex-fast on|off|en|zh|ja|ko]",
"warning",
);
},
Expand Down
2 changes: 1 addition & 1 deletion extensions/command-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const HISTORY_DIR = join(homedir(), ".pi", "folder-history");
const MAX_HISTORY = 500;

function getHistoryFile(cwd: string): string {
const name = cwd.replace(/\//g, "-");
const name = cwd.replace(/[\\\/:]/g, "-");
return join(HISTORY_DIR, `${name}.jsonl`);
}

Expand Down
56 changes: 32 additions & 24 deletions extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { restoreCapyToolsSettings, getCapyToolsSettings } from "./capy-tools-config.ts";
import fetchExtension from "./fetch.ts";
import enableBuiltinSearchExtension from "./enable-builtin-search.ts";
import repoMapExtension from "./repo-map.ts";
Expand All @@ -25,34 +26,41 @@ import showsignatureExtension from "./showsignature.ts";
import workingMessageExtension from "./cat-whimsical/index.ts";

export default async function piBasicToolsExtension(pi: ExtensionAPI): Promise<void> {
// Load all tools through one entrypoint so shared renderer state is truly shared.
enableBuiltinSearchExtension(pi);
fetchExtension(pi);
repoMapExtension(pi);
readBlockExtension(pi);
symbolOutlineExtension(pi);
applyPatchExtension(pi);
terminalSessionExtension(pi);
askUserExtension(pi);
askQuestionExtension(pi);
askQuestionnaireExtension(pi);
sourcegraphExtension(pi);
recapExtension(pi);
// Restore user config before deciding which tools to load.
await restoreCapyToolsSettings();
const tools = getCapyToolsSettings().tools;

const enabled = (id: string): boolean => tools[id as keyof typeof tools] !== false;

// enable-builtin-search is always loaded (needed for settings UI tool toggles)
if (enabled("enable-builtin-search")) enableBuiltinSearchExtension(pi);
if (enabled("fetch")) fetchExtension(pi);
if (enabled("repo-map")) repoMapExtension(pi);
if (enabled("read-block")) readBlockExtension(pi);
if (enabled("symbol-outline")) symbolOutlineExtension(pi);
if (enabled("apply-patch")) applyPatchExtension(pi);
if (enabled("terminal-session")) terminalSessionExtension(pi);
if (enabled("ask-user")) askUserExtension(pi);
if (enabled("ask-question")) askQuestionExtension(pi);
if (enabled("ask-questionnaire")) askQuestionnaireExtension(pi);
if (enabled("sourcegraph")) sourcegraphExtension(pi);
if (enabled("recap")) recapExtension(pi);
// Opt-in diagnostic: no-op unless PI_BASIC_TOOLS_DIAG_SHAPES is set.
messageShapeDiagnosticExtension(pi);
autoCompactExtension(pi);
codexFastExtension(pi);
if (enabled("message-shape-diagnostic")) messageShapeDiagnosticExtension(pi);
if (enabled("auto-compact")) autoCompactExtension(pi);
if (enabled("codex-fast")) codexFastExtension(pi);
// capy-tools-settings MUST always load — otherwise users can't re-enable tools
capyToolsSettingsExtension(pi);
commandHistoryExtension(pi);
effortsExtension(pi);
codexGoalExtension(pi);
await rtkExtension(pi);
thinkingStepsExtension(pi);
todoExtension(pi);
showsignatureExtension(pi);
if (enabled("command-history")) commandHistoryExtension(pi);
if (enabled("efforts")) effortsExtension(pi);
if (enabled("codex-goal")) codexGoalExtension(pi);
if (enabled("rtk")) await rtkExtension(pi);
if (enabled("thinking-steps")) thinkingStepsExtension(pi);
if (enabled("todo")) todoExtension(pi);
if (enabled("showsignature")) showsignatureExtension(pi);
// Registered AFTER todoExtension so the Capy Tools working message sits
// below the todo overlay in pi's UI (forked from
// https://github.com/lulucatdev/pi-cat-whimsical, MIT). See
// extensions/cat-whimsical/index.ts header for full attribution.
workingMessageExtension(pi);
if (enabled("working-message")) workingMessageExtension(pi);
}
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"access": "public"
},
"scripts": {
"postinstall": "npm install --omit=dev 2>/dev/null || true",
"test": "bun test",
"test:build": "bun build extensions/index.ts extensions/fetch.ts extensions/enable-builtin-search.ts extensions/repo-map.ts extensions/read-block.ts extensions/symbol-outline.ts extensions/apply-patch.ts extensions/terminal-session.ts extensions/ask-user.ts extensions/ask-question.ts extensions/ask-questionnaire.ts extensions/sourcegraph.ts extensions/recap.ts extensions/message-shape-diagnostic.ts extensions/auto-compact.ts extensions/codex-fast.ts extensions/capy-tools-config.ts extensions/capy-tools-settings.ts extensions/command-history.ts extensions/efforts/index.ts extensions/codex-goal/index.ts extensions/rtk/index.ts extensions/thinking-steps/index.ts extensions/todo/index.ts extensions/cat-whimsical/index.ts extensions/showsignature.ts --target=node --external @earendil-works/pi-coding-agent --external @earendil-works/pi-ai --external @earendil-works/pi-tui --external @sinclair/typebox --outdir /tmp/capy-tools-build",
"test:tui-capture": "python3 scripts/capture-pi-tui.py --expect 'Explored 3 targets' --expect Outline --expect Read --expect Search --expect-tools-block Outline --expect-tools-block Read --expect-tools-block Search --forbid 'grep grep'",
Expand All @@ -45,6 +46,9 @@
"./skills"
]
},
"dependencies": {
"typescript": "^6.0.3"
},
"peerDependencies": {
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*",
Expand All @@ -55,7 +59,6 @@
"@earendil-works/pi-ai": "^0.74.0",
"@earendil-works/pi-coding-agent": "^0.74.0",
"@earendil-works/pi-tui": "^0.74.0",
"@sinclair/typebox": "^0.34.49",
"typescript": "^6.0.3"
"@sinclair/typebox": "^0.34.49"
}
}