Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,14 @@ Advanced options remain in `config.json`.

```text
/tool-display show # Show the effective config summary
/tool-display reset # Reset to the default opencode preset
/tool-display preset opencode # Apply opencode preset
/tool-display preset balanced # Apply balanced preset
/tool-display preset verbose # Apply verbose preset
/tool-display reset # Reset the current effective scope to opencode
/tool-display reset --global # Reset global config
/tool-display reset --project # Reset project config (trusted projects only)
/tool-display preset opencode # Apply opencode preset to the current effective scope
/tool-display preset balanced # Apply balanced preset to the current effective scope
/tool-display preset verbose # Apply verbose preset to the current effective scope
/tool-display preset verbose --global # Apply preset to global config
/tool-display preset verbose --project # Apply preset to project config (trusted projects only)
```

### Tool display adapter API
Expand Down Expand Up @@ -124,11 +128,12 @@ import { decorateToolForDisplay, decorateMcpToolForDisplay } from "pi-tool-displ
Runtime configuration is stored at:

```text
Default global path: ~/.pi/agent/extensions/pi-tool-display/config.json
Actual global path: $PI_CODING_AGENT_DIR/extensions/pi-tool-display/config.json when PI_CODING_AGENT_DIR is set
Global default: ~/.pi/agent/extensions/pi-tool-display/config.json
Global with PI_CODING_AGENT_DIR: $PI_CODING_AGENT_DIR/extensions/pi-tool-display/config.json
Project override: .pi/extensions/pi-tool-display/config.json
```

A starter template is included at `config/config.example.json`.
Effective configuration is merged as defaults → global config → project config. Project-level config requires Pi 0.79.1 or newer so the extension can verify project trust status; when trust is unavailable or the project is not trusted, project config is ignored with a warning. Project saves store only values that differ from the global config so inherited global settings keep applying. A starter template is included at `config/config.example.json`.

### Configuration options

Expand Down Expand Up @@ -268,7 +273,7 @@ Notes:

### Debug logging

Debug logging is disabled by default. Set `debug` to `true` in the extension root `config.json` only when collecting diagnostics; missing or non-`true` values are treated as `false`. When enabled, diagnostics are appended to `debug/debug.log` under a runtime-created `debug/` directory, and no debug output is written to the terminal.
Debug logging is disabled by default. Set `debug` to `true` in the global or active project `config.json` only when collecting diagnostics; missing or non-`true` values are treated as `false`. When enabled, diagnostics are appended to `debug/debug.log` next to the active config file, and no debug output is written to the terminal.

## Rendering notes

Expand Down Expand Up @@ -321,9 +326,11 @@ Built-in tool overrides (including `bash`) are registered with deferred ownershi

If your settings are not being applied:

1. Check that the global Pi tool-display config exists (default: `~/.pi/agent/extensions/pi-tool-display/config.json`, respects `PI_CODING_AGENT_DIR`)
2. Make sure the JSON is valid
3. Run `/tool-display show` to inspect the effective config summary
1. Check the global Pi tool-display config (default: `~/.pi/agent/extensions/pi-tool-display/config.json`, respects `PI_CODING_AGENT_DIR`)
2. Check the optional project override at `.pi/extensions/pi-tool-display/config.json`
3. Make sure the JSON is valid
4. Make sure you are running Pi 0.79.1 or newer and the project is trusted when expecting project overrides to apply
5. Run `/tool-display show` to inspect the effective config summary

### MCP or custom tool rendering not appearing

Expand Down
189 changes: 189 additions & 0 deletions src/config-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type {
ExtensionAPI,
ExtensionCommandContext,
} from "@earendil-works/pi-coding-agent";
import {
getToolDisplayDebugPaths,
loadEffectiveToolDisplayConfig,
loadToolDisplayConfig,
normalizeToolDisplayConfig,
saveToolDisplayConfig,
saveToolDisplayConfigOverlay,
type EffectiveToolDisplayConfigLoadResult,
type ToolDisplayConfigScope,
} from "./config-store.js";
import {
applyCapabilityConfigGuards,
detectToolDisplayCapabilities,
type ToolDisplayCapabilities,
} from "./capabilities.js";
import type { ToolDisplayDebugRuntimeConfig } from "./debug-logger.js";
import {
BUILT_IN_TOOL_OVERRIDE_NAMES,
type ToolDisplayConfig,
} from "./types.js";

export interface ToolDisplayRuntimeConfigController {
getConfig(): ToolDisplayConfig;
getConfigPath(): string;
getCapabilities(): ToolDisplayCapabilities;
getEffectiveConfig(): ToolDisplayConfig;
getDebugRuntimeConfig(): ToolDisplayDebugRuntimeConfig;
refreshFromContext(ctx: unknown): void;
refreshCapabilitiesFromContext(ctx: unknown): void;
consumePendingLoadWarnings(): string[];
setConfig(
next: ToolDisplayConfig,
ctx: ExtensionCommandContext,
options?: { scope?: ToolDisplayConfigScope },
): boolean;
}

function ownershipChanged(
previous: ToolDisplayConfig,
next: ToolDisplayConfig,
): boolean {
return BUILT_IN_TOOL_OVERRIDE_NAMES.some(
(toolName) =>
previous.registerToolOverrides[toolName] !==
next.registerToolOverrides[toolName],
);
}

function getContextCwd(ctx: unknown): string {
const cwd = (ctx as { cwd?: unknown } | undefined)?.cwd;
return typeof cwd === "string" && cwd.length > 0 ? cwd : process.cwd();
}

function getContextProjectTrust(ctx: unknown): { trusted: boolean; trustApiAvailable: boolean } {
const isProjectTrusted = (ctx as { isProjectTrusted?: unknown } | undefined)?.isProjectTrusted;
if (typeof isProjectTrusted !== "function") {
return { trusted: false, trustApiAvailable: false };
}

try {
return { trusted: isProjectTrusted() === true, trustApiAvailable: true };
} catch {
return { trusted: false, trustApiAvailable: true };
}
}

export function createToolDisplayConfigController(pi: ExtensionAPI): ToolDisplayRuntimeConfigController {
let currentCwd = process.cwd();
let currentProjectTrusted = false;
let currentProjectTrustApiAvailable = false;
let configLoad: EffectiveToolDisplayConfigLoadResult = loadEffectiveToolDisplayConfig({
cwd: currentCwd,
projectTrusted: currentProjectTrusted,
});
let config: ToolDisplayConfig = configLoad.config;
let pendingLoadWarnings = [...configLoad.warnings];
let capabilities: ToolDisplayCapabilities = {
hasMcpTooling: false,
hasRtkOptimizer: false,
};

const reloadConfig = (): void => {
configLoad = loadEffectiveToolDisplayConfig({
cwd: currentCwd,
projectTrusted: currentProjectTrusted,
});
config = configLoad.config;
pendingLoadWarnings = [...configLoad.warnings];
};

const explainMissingTrustApi = (): void => {
if (currentProjectTrustApiAvailable || !configLoad.projectConfigIgnored) {
return;
}

pendingLoadWarnings = pendingLoadWarnings.filter(
(warning) => !warning.startsWith("Ignored untrusted project tool-display config:"),
);
pendingLoadWarnings.push(
`Project-level tool-display configs are only supported in Pi 0.79.1 or newer; ignored ${configLoad.projectConfigFile}. Upgrade Pi or use global config instead.`,
);
};

const refreshCapabilities = (cwd = currentCwd): void => {
capabilities = detectToolDisplayCapabilities(pi, cwd);
};

return {
getConfig: () => config,
getConfigPath: () => configLoad.activeConfigFile,
getCapabilities: () => capabilities,
getEffectiveConfig: () => applyCapabilityConfigGuards(config, capabilities),
getDebugRuntimeConfig: () => ({
debug: config.debug,
...getToolDisplayDebugPaths(configLoad.activeConfigFile),
}),
refreshFromContext(ctx: unknown): void {
currentCwd = getContextCwd(ctx);
const projectTrust = getContextProjectTrust(ctx);
currentProjectTrusted = projectTrust.trusted;
currentProjectTrustApiAvailable = projectTrust.trustApiAvailable;
reloadConfig();
explainMissingTrustApi();
refreshCapabilities(currentCwd);
},
refreshCapabilitiesFromContext(ctx: unknown): void {
refreshCapabilities(getContextCwd(ctx));
},
consumePendingLoadWarnings(): string[] {
const warnings = pendingLoadWarnings;
pendingLoadWarnings = [];
return warnings;
},
setConfig(
next: ToolDisplayConfig,
ctx: ExtensionCommandContext,
options?: { scope?: ToolDisplayConfigScope },
): boolean {
const normalized = normalizeToolDisplayConfig(next);
const selectedScope = options?.scope ?? configLoad.activeScope;
const targetConfigFile = selectedScope === "project"
? configLoad.projectConfigFile
: configLoad.globalConfigFile;

if (selectedScope === "project" && !currentProjectTrusted) {
const message = currentProjectTrustApiAvailable
? "Cannot save project tool-display config because this project is not trusted."
: "Project-level tool-display configs are only supported in Pi 0.79.1 or newer; cannot save project config. Upgrade Pi or use global config instead.";
ctx.ui.notify(message, "warning");
return false;
}

if (!targetConfigFile) {
ctx.ui.notify(`Cannot resolve ${selectedScope} tool-display config path.`, "error");
return false;
}

const previous = config;
const saved = selectedScope === "project"
? saveToolDisplayConfigOverlay(
normalized,
loadToolDisplayConfig(configLoad.globalConfigFile).config,
targetConfigFile,
)
: saveToolDisplayConfig(normalized, targetConfigFile);
if (!saved.success) {
if (saved.error) {
ctx.ui.notify(saved.error, "error");
}
return false;
}

reloadConfig();

if (ownershipChanged(previous, config)) {
ctx.ui.notify(
"Tool ownership updates apply after /reload.",
"warning",
);
}

return true;
},
};
}
60 changes: 48 additions & 12 deletions src/config-modal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import type { ToolDisplayCapabilities } from "./capabilities.js";
import { getToolDisplayConfigPath } from "./config-store.js";
import { getToolDisplayConfigPath, type ToolDisplayConfigScope } from "./config-store.js";
import {
detectToolDisplayPreset,
getToolDisplayPresetConfig,
Expand All @@ -14,8 +14,9 @@ import { type ToolDisplayConfig } from "./types.js";

interface ToolDisplayConfigController {
getConfig(): ToolDisplayConfig;
setConfig(next: ToolDisplayConfig, ctx: ExtensionCommandContext): void;
setConfig(next: ToolDisplayConfig, ctx: ExtensionCommandContext, options?: { scope?: ToolDisplayConfigScope }): boolean | void;
getCapabilities(): ToolDisplayCapabilities;
getConfigPath?(): string;
}

interface ModalOverlayOptions {
Expand Down Expand Up @@ -75,6 +76,31 @@ function parseNumber(value: string, fallback: number): number {
return Number.isNaN(parsed) ? fallback : parsed;
}

function extractScopeFlag(raw: string): { command: string; scope?: ToolDisplayConfigScope } {
const tokens = raw.split(/\s+/).filter(Boolean);
let scope: ToolDisplayConfigScope | undefined;
const commandTokens: string[] = [];

for (const token of tokens) {
const normalized = token.toLowerCase();
if (normalized === "--project") {
scope = "project";
continue;
}
if (normalized === "--global") {
scope = "global";
continue;
}
if (normalized === "--effective") {
scope = undefined;
continue;
}
commandTokens.push(token);
}

return { command: commandTokens.join(" "), scope };
}

function buildAdvancedNotes(
config: ToolDisplayConfig,
capabilities: ToolDisplayCapabilities,
Expand All @@ -92,8 +118,9 @@ function buildAdvancedNotes(
function buildInspectorSettings(
config: ToolDisplayConfig,
capabilities: ToolDisplayCapabilities,
activeConfigPath = getToolDisplayConfigPath(),
): InspectorSettingItem[] {
const configPath = shortenPath(getToolDisplayConfigPath());
const configPath = shortenPath(activeConfigPath);
const items: InspectorSettingItem[] = [
{
id: "preset",
Expand Down Expand Up @@ -314,15 +341,18 @@ function buildInspectorSettings(
return items;
}

function applyPreset(preset: ToolDisplayPreset): ToolDisplayConfig {
return getToolDisplayPresetConfig(preset);
function applyPreset(preset: ToolDisplayPreset, currentConfig?: ToolDisplayConfig): ToolDisplayConfig {
const presetConfig = getToolDisplayPresetConfig(preset);
return currentConfig
? { ...presetConfig, debug: currentConfig.debug }
: presetConfig;
}

function applySetting(config: ToolDisplayConfig, id: string, value: string): ToolDisplayConfig {
switch (id) {
case "preset": {
const parsed = parseToolDisplayPreset(value);
return parsed ? applyPreset(parsed) : config;
return parsed ? applyPreset(parsed, config) : config;
}
case "enableNativeUserMessageBox":
return {
Expand Down Expand Up @@ -414,7 +444,7 @@ export async function openSettingsModal(ctx: ExtensionCommandContext, controller
(tui, theme, _keybindings, done) => {
const inspector = new SplitPaneInspectorModal(
{
getSettings: () => buildInspectorSettings(controller.getConfig(), capabilities),
getSettings: () => buildInspectorSettings(controller.getConfig(), capabilities, controller.getConfigPath?.()),
onChange: (id, newValue) => {
const next = applySetting(controller.getConfig(), id, newValue);
controller.setConfig(next, ctx);
Expand Down Expand Up @@ -458,7 +488,9 @@ export function handleToolDisplayArgs(args: string, ctx: ExtensionCommandContext
return false;
}

const normalized = raw.toLowerCase();
const parsedArgs = extractScopeFlag(raw);
const normalized = parsedArgs.command.toLowerCase();
const setOptions = parsedArgs.scope ? { scope: parsedArgs.scope } : undefined;

if (normalized === "show") {
ctx.ui.notify(
Expand All @@ -469,8 +501,10 @@ export function handleToolDisplayArgs(args: string, ctx: ExtensionCommandContext
}

if (normalized === "reset") {
controller.setConfig(getToolDisplayPresetConfig("opencode"), ctx);
ctx.ui.notify("Tool display preset reset to opencode.", "info");
const saved = controller.setConfig(applyPreset("opencode", controller.getConfig()), ctx, setOptions);
if (saved !== false) {
ctx.ui.notify("Tool display preset reset to opencode.", "info");
}
return true;
}

Expand All @@ -482,8 +516,10 @@ export function handleToolDisplayArgs(args: string, ctx: ExtensionCommandContext
return true;
}

controller.setConfig(getToolDisplayPresetConfig(preset), ctx);
ctx.ui.notify(`Tool display preset set to ${preset}.`, "info");
const saved = controller.setConfig(applyPreset(preset, controller.getConfig()), ctx, setOptions);
if (saved !== false) {
ctx.ui.notify(`Tool display preset set to ${preset}.`, "info");
}
return true;
}

Expand Down
Loading