Add pi-tool-display (compact tool rendering, adaptive diffs) + pi-lsp-extension (LSP diagnostics/hover/definitions) — vendored from npm 0.5.0 / 1.3.0
This commit is contained in:
514
extensions/pi-tool-display/src/config-modal.ts
Normal file
514
extensions/pi-tool-display/src/config-modal.ts
Normal file
@@ -0,0 +1,514 @@
|
||||
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
||||
import type { ToolDisplayCapabilities } from "./capabilities.js";
|
||||
import { getToolDisplayConfigPath } from "./config-store.js";
|
||||
import {
|
||||
detectToolDisplayPreset,
|
||||
getToolDisplayPresetConfig,
|
||||
parseToolDisplayPreset,
|
||||
TOOL_DISPLAY_PRESETS,
|
||||
type ToolDisplayPreset,
|
||||
} from "./presets.js";
|
||||
import { shortenPath } from "./render-utils.js";
|
||||
import type { InspectorSettingItem } from "./settings-inspector-modal.js";
|
||||
import { type ToolDisplayConfig } from "./types.js";
|
||||
|
||||
interface ToolDisplayConfigController {
|
||||
getConfig(): ToolDisplayConfig;
|
||||
setConfig(next: ToolDisplayConfig, ctx: ExtensionCommandContext): void;
|
||||
getCapabilities(): ToolDisplayCapabilities;
|
||||
}
|
||||
|
||||
interface ModalOverlayOptions {
|
||||
anchor: "center";
|
||||
width: number;
|
||||
maxHeight: number;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
const PREVIEW_LINE_VALUES = ["4", "8", "12", "20", "40"] as const;
|
||||
const BASH_PREVIEW_LINE_VALUES = ["0", "5", "10", "20", "40"] as const;
|
||||
const PRESET_COMMAND_HINT = TOOL_DISPLAY_PRESETS.join("|");
|
||||
|
||||
function toOnOff(value: boolean): string {
|
||||
return value ? "on" : "off";
|
||||
}
|
||||
|
||||
function toolOwnershipSummary(config: ToolDisplayConfig): string {
|
||||
const overrides = config.registerToolOverrides;
|
||||
return `read:${toOnOff(overrides.read)},grep:${toOnOff(overrides.grep)},find:${toOnOff(overrides.find)},ls:${toOnOff(overrides.ls)},bash:${toOnOff(overrides.bash)},edit:${toOnOff(overrides.edit)},write:${toOnOff(overrides.write)}`;
|
||||
}
|
||||
|
||||
function summarizeConfig(config: ToolDisplayConfig, capabilities: ToolDisplayCapabilities): string {
|
||||
const preset = detectToolDisplayPreset(config);
|
||||
const parts = [
|
||||
`preset=${preset}`,
|
||||
`owners={${toolOwnershipSummary(config)}}`,
|
||||
`userBox=${toOnOff(config.enableNativeUserMessageBox)}`,
|
||||
`read=${config.readOutputMode}`,
|
||||
`search=${config.searchOutputMode}`,
|
||||
`preview=${config.previewLines}`,
|
||||
`expandedMax=${config.expandedPreviewMaxLines}`,
|
||||
`bash=${config.bashOutputMode}`,
|
||||
`bashLines=${config.bashCollapsedLines}`,
|
||||
`diff=${config.diffViewMode}/${config.diffIndicatorMode}@${config.diffSplitMinWidth}`,
|
||||
`diffLines=${config.diffCollapsedLines}`,
|
||||
`diffWrap=${toOnOff(config.diffWordWrap)}`,
|
||||
];
|
||||
|
||||
if (capabilities.hasMcpTooling) {
|
||||
parts.push(`mcp=${config.mcpOutputMode}`);
|
||||
} else {
|
||||
parts.push("mcp=auto-hidden");
|
||||
}
|
||||
|
||||
if (capabilities.hasRtkOptimizer) {
|
||||
parts.push(`rtkHints=${toOnOff(config.showRtkCompactionHints)}`);
|
||||
} else {
|
||||
parts.push("rtkHints=auto-off");
|
||||
}
|
||||
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
function parseNumber(value: string, fallback: number): number {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? fallback : parsed;
|
||||
}
|
||||
|
||||
function buildAdvancedNotes(
|
||||
config: ToolDisplayConfig,
|
||||
capabilities: ToolDisplayCapabilities,
|
||||
extra: readonly string[],
|
||||
): string[] {
|
||||
const notes = [
|
||||
...extra,
|
||||
"Manual JSON edits also expose registerToolOverrides.*, expandedPreviewMaxLines, diffSplitMinWidth, diffCollapsedLines, diffIndicatorMode, and diffWordWrap.",
|
||||
`Tool ownership is currently ${toolOwnershipSummary(config)} and still applies after /reload.`,
|
||||
`Truncation hints are ${toOnOff(config.showTruncationHints)}${capabilities.hasRtkOptimizer ? `; RTK hints are ${toOnOff(config.showRtkCompactionHints)}.` : "."}`,
|
||||
];
|
||||
return notes;
|
||||
}
|
||||
|
||||
function buildInspectorSettings(
|
||||
config: ToolDisplayConfig,
|
||||
capabilities: ToolDisplayCapabilities,
|
||||
): InspectorSettingItem[] {
|
||||
const configPath = shortenPath(getToolDisplayConfigPath());
|
||||
const items: InspectorSettingItem[] = [
|
||||
{
|
||||
id: "preset",
|
||||
label: "Preset profile",
|
||||
currentValue: detectToolDisplayPreset(config),
|
||||
values: TOOL_DISPLAY_PRESETS,
|
||||
inspectorTitle: "Preset Profile",
|
||||
inspectorSummary: [
|
||||
"Determines the overall verbosity and layout of the agent's tool output.",
|
||||
"Choosing a preset applies a coherent profile across read, search, MCP, bash, and diff display settings.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"opencode — strict inline-only tool output",
|
||||
"balanced — compact summaries with counts",
|
||||
"verbose — larger line previews and more visible bash output",
|
||||
"custom — shown automatically when manual overrides no longer match a preset",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"Presets reset multiple fields together, so manual JSON tuning is the right place for durable custom combinations.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["verbosity", "profile", "layout", "custom", ...TOOL_DISPLAY_PRESETS],
|
||||
},
|
||||
{
|
||||
id: "readOutputMode",
|
||||
label: "Read tool output",
|
||||
currentValue: config.readOutputMode,
|
||||
values: ["hidden", "summary", "preview"],
|
||||
inspectorTitle: "Read Tool Output",
|
||||
inspectorSummary: [
|
||||
"Controls how read results appear inline after the tool call header.",
|
||||
"Use hidden for the cleanest transcript, summary for file metrics, or preview when seeing source lines matters in-context.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"hidden — path and status only",
|
||||
"summary — adds compact file metrics",
|
||||
"preview — shows the first configured preview lines",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"expandedPreviewMaxLines bounds how many lines can appear after expanding a preview-heavy read result.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["file", "source", "preview", "summary", "hidden"],
|
||||
},
|
||||
{
|
||||
id: "searchOutputMode",
|
||||
label: "Grep/Find/Ls output",
|
||||
currentValue: config.searchOutputMode,
|
||||
values: ["hidden", "count", "preview"],
|
||||
inspectorTitle: "Grep / Find / Ls Output",
|
||||
inspectorSummary: [
|
||||
"Controls how search-style tools compress their result sets inside the transcript.",
|
||||
"Count mode keeps discovery actions readable while still surfacing how much data the tool matched.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"hidden — call header only",
|
||||
"count — totals only for matches or entries",
|
||||
"preview — shows the first configured preview lines",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"Preview-heavy search output is most effective when paired with larger previewLines values in custom configurations.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["grep", "find", "ls", "matches", "count", "results"],
|
||||
},
|
||||
];
|
||||
|
||||
if (capabilities.hasMcpTooling) {
|
||||
items.push({
|
||||
id: "mcpOutputMode",
|
||||
label: "MCP tool output",
|
||||
currentValue: config.mcpOutputMode,
|
||||
values: ["hidden", "summary", "preview"],
|
||||
inspectorTitle: "MCP Tool Output",
|
||||
inspectorSummary: [
|
||||
"Controls how proxied MCP tool results are compacted when they return text output.",
|
||||
"Summary mode is the safest default when you want awareness without flooding the chat pane.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"hidden — call metadata only",
|
||||
"summary — compact line-count summary",
|
||||
"preview — shows the first configured preview lines",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"This control appears only when MCP tooling is available in the current Pi session.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["mcp", "proxy", "server", "summary", "preview"],
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: "previewLines",
|
||||
label: "Preview lines",
|
||||
currentValue: String(config.previewLines),
|
||||
values: PREVIEW_LINE_VALUES,
|
||||
inspectorTitle: "Preview Lines",
|
||||
inspectorSummary: [
|
||||
"Sets how many lines appear when read, search, MCP, or bash preview modes are collapsed inline.",
|
||||
"Accepted manual range: 1 to 80 lines. The quick selector cycles through a curated set for fast tuning.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"Lower values keep transcripts dense and skimmable",
|
||||
"Higher values surface more source context before expansion",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"Pair this with expandedPreviewMaxLines when you want larger expanded previews without making collapsed output noisy.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["preview", "lines", "range", "collapsed", "read", "grep", "mcp", "bash"],
|
||||
},
|
||||
{
|
||||
id: "bashOutputMode",
|
||||
label: "Bash tool output",
|
||||
currentValue: config.bashOutputMode,
|
||||
values: ["opencode", "summary", "preview"],
|
||||
inspectorTitle: "Bash Tool Output",
|
||||
inspectorSummary: [
|
||||
"Controls how shell command output is rendered when the command finishes successfully.",
|
||||
"The opencode mode keeps command output recognizable while still compressing walls of stdout.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"opencode — Pi/OpenCode-style collapsed bash view",
|
||||
"summary — output count only",
|
||||
"preview — uses the shared previewLines setting",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"Quiet commands still collapse aggressively, so mode selection matters most on verbose build, test, and script output.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["bash", "shell", "stdout", "command", "opencode"],
|
||||
},
|
||||
{
|
||||
id: "bashCollapsedLines",
|
||||
label: "Bash collapsed lines",
|
||||
currentValue: String(config.bashCollapsedLines),
|
||||
values: BASH_PREVIEW_LINE_VALUES,
|
||||
inspectorTitle: "Bash Collapsed Lines",
|
||||
inspectorSummary: [
|
||||
"Sets the inline line budget used specifically by opencode bash mode before expansion.",
|
||||
"Accepted manual range: 0 to 80 lines. Setting 0 hides collapsed bash output entirely while keeping the command visible.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"0 — hide collapsed bash output",
|
||||
"5/10/20/40 — progressively larger inline command previews",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"This setting only changes the opencode bash renderer; preview mode continues to use previewLines instead.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["bash", "collapsed", "lines", "stdout", "zero"],
|
||||
},
|
||||
{
|
||||
id: "diffViewMode",
|
||||
label: "Edit diff layout",
|
||||
currentValue: config.diffViewMode,
|
||||
values: ["auto", "split", "unified"],
|
||||
inspectorTitle: "Edit Diff Layout",
|
||||
inspectorSummary: [
|
||||
"Controls how edit and write diffs are arranged when the extension renders code changes.",
|
||||
"Auto mode adapts to terminal width so wide panes get side-by-side diffs while narrow panes stay readable.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"auto — adaptive layout based on available width",
|
||||
"split — force side-by-side diff columns",
|
||||
"unified — force a single-column diff",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"Manual JSON tuning exposes diffSplitMinWidth, diffCollapsedLines, diffIndicatorMode, and diffWordWrap for more aggressive diff control.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["diff", "edit", "write", "split", "unified", "auto"],
|
||||
},
|
||||
{
|
||||
id: "diffIndicatorMode",
|
||||
label: "Diff indicators",
|
||||
currentValue: config.diffIndicatorMode,
|
||||
values: ["bars", "classic", "none"],
|
||||
inspectorTitle: "Diff Indicators",
|
||||
inspectorSummary: [
|
||||
"Controls whether changed diff lines use vertical bars, classic +/- markers, or no indicators at all.",
|
||||
"Bars continue across wrapped changed rows, classic markers appear only on the first wrapped row, and none removes the indicator column styling.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"bars — persistent vertical indicators for changed rows",
|
||||
"classic — + / - markers on the first visual row only",
|
||||
"none — no diff indicator marker",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"Use config.json when you want this indicator preference to remain explicit alongside other diff rendering overrides.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["diff", "indicator", "bars", "classic", "none", "marker"],
|
||||
},
|
||||
{
|
||||
id: "enableNativeUserMessageBox",
|
||||
label: "Native user message box",
|
||||
currentValue: toOnOff(config.enableNativeUserMessageBox),
|
||||
values: ["on", "off"],
|
||||
inspectorTitle: "Native User Message Box",
|
||||
inspectorSummary: [
|
||||
"Toggles the bordered native renderer used for user prompts inside the Pi transcript.",
|
||||
"Keep it on when you want clearer message separation, or turn it off to fall back to Pi's default user message rendering.",
|
||||
],
|
||||
inspectorOptions: [
|
||||
"on — bordered native user prompt box",
|
||||
"off — default Pi prompt rendering",
|
||||
],
|
||||
inspectorAdvanced: buildAdvancedNotes(config, capabilities, [
|
||||
"This switch only affects presentation. It does not change stored prompts, markdown handling, or tool behavior.",
|
||||
]),
|
||||
inspectorPath: configPath,
|
||||
searchTerms: ["user", "message", "box", "prompt", "native"],
|
||||
},
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function applyPreset(preset: ToolDisplayPreset): ToolDisplayConfig {
|
||||
return getToolDisplayPresetConfig(preset);
|
||||
}
|
||||
|
||||
function applySetting(config: ToolDisplayConfig, id: string, value: string): ToolDisplayConfig {
|
||||
switch (id) {
|
||||
case "preset": {
|
||||
const parsed = parseToolDisplayPreset(value);
|
||||
return parsed ? applyPreset(parsed) : config;
|
||||
}
|
||||
case "enableNativeUserMessageBox":
|
||||
return {
|
||||
...config,
|
||||
enableNativeUserMessageBox: value === "on",
|
||||
};
|
||||
case "readOutputMode":
|
||||
return {
|
||||
...config,
|
||||
readOutputMode: value as ToolDisplayConfig["readOutputMode"],
|
||||
};
|
||||
case "searchOutputMode":
|
||||
return {
|
||||
...config,
|
||||
searchOutputMode: value as ToolDisplayConfig["searchOutputMode"],
|
||||
};
|
||||
case "mcpOutputMode":
|
||||
return {
|
||||
...config,
|
||||
mcpOutputMode: value as ToolDisplayConfig["mcpOutputMode"],
|
||||
};
|
||||
case "previewLines":
|
||||
return {
|
||||
...config,
|
||||
previewLines: parseNumber(value, config.previewLines),
|
||||
};
|
||||
case "bashOutputMode":
|
||||
return {
|
||||
...config,
|
||||
bashOutputMode: value as ToolDisplayConfig["bashOutputMode"],
|
||||
};
|
||||
case "bashCollapsedLines":
|
||||
return {
|
||||
...config,
|
||||
bashCollapsedLines: parseNumber(value, config.bashCollapsedLines),
|
||||
};
|
||||
case "diffViewMode":
|
||||
return {
|
||||
...config,
|
||||
diffViewMode: value as ToolDisplayConfig["diffViewMode"],
|
||||
};
|
||||
case "diffIndicatorMode":
|
||||
return {
|
||||
...config,
|
||||
diffIndicatorMode: value as ToolDisplayConfig["diffIndicatorMode"],
|
||||
};
|
||||
default:
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveResponsiveOverlayOptions(): ModalOverlayOptions {
|
||||
const terminalWidth =
|
||||
typeof process.stdout.columns === "number" && Number.isFinite(process.stdout.columns)
|
||||
? process.stdout.columns
|
||||
: 120;
|
||||
const terminalHeight =
|
||||
typeof process.stdout.rows === "number" && Number.isFinite(process.stdout.rows)
|
||||
? process.stdout.rows
|
||||
: 36;
|
||||
|
||||
const margin = 1;
|
||||
const availableWidth = Math.max(72, terminalWidth - margin * 2);
|
||||
const preferredWidth = terminalWidth >= 170 ? 128 : terminalWidth >= 145 ? 118 : terminalWidth >= 120 ? 106 : 92;
|
||||
const width = Math.max(72, Math.min(preferredWidth, availableWidth));
|
||||
|
||||
const availableHeight = Math.max(14, terminalHeight - margin * 2);
|
||||
const preferredHeight = Math.max(14, Math.floor(terminalHeight * 0.78));
|
||||
const maxHeight = Math.min(preferredHeight, availableHeight);
|
||||
|
||||
return {
|
||||
anchor: "center",
|
||||
width,
|
||||
maxHeight,
|
||||
margin,
|
||||
};
|
||||
}
|
||||
|
||||
export async function openSettingsModal(ctx: ExtensionCommandContext, controller: ToolDisplayConfigController): Promise<void> {
|
||||
const overlayOptions = resolveResponsiveOverlayOptions();
|
||||
const capabilities = controller.getCapabilities();
|
||||
|
||||
const [{ ZellijModal }, { SplitPaneInspectorModal }] = await Promise.all([
|
||||
import("./zellij-modal.js"),
|
||||
import("./settings-inspector-modal.js"),
|
||||
]);
|
||||
|
||||
await ctx.ui.custom<void>(
|
||||
(tui, theme, _keybindings, done) => {
|
||||
const inspector = new SplitPaneInspectorModal(
|
||||
{
|
||||
getSettings: () => buildInspectorSettings(controller.getConfig(), capabilities),
|
||||
onChange: (id, newValue) => {
|
||||
const next = applySetting(controller.getConfig(), id, newValue);
|
||||
controller.setConfig(next, ctx);
|
||||
},
|
||||
onClose: () => done(),
|
||||
},
|
||||
theme,
|
||||
);
|
||||
|
||||
const modal = new ZellijModal(
|
||||
inspector,
|
||||
{
|
||||
borderStyle: "square",
|
||||
padding: 0,
|
||||
titleBar: {},
|
||||
overlay: overlayOptions,
|
||||
},
|
||||
theme,
|
||||
);
|
||||
|
||||
return {
|
||||
render: (width: number) => modal.renderModal(width).lines,
|
||||
invalidate: () => modal.invalidate(),
|
||||
handleInput(data: string) {
|
||||
modal.handleInput(data);
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
},
|
||||
{ overlay: true, overlayOptions },
|
||||
);
|
||||
}
|
||||
|
||||
export function handleToolDisplayArgs(args: string, ctx: ExtensionCommandContext, controller: ToolDisplayConfigController): boolean {
|
||||
const raw = args.trim();
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalized = raw.toLowerCase();
|
||||
|
||||
if (normalized === "show") {
|
||||
ctx.ui.notify(
|
||||
`tool-display: ${summarizeConfig(controller.getConfig(), controller.getCapabilities())}`,
|
||||
"info",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalized === "reset") {
|
||||
controller.setConfig(getToolDisplayPresetConfig("opencode"), ctx);
|
||||
ctx.ui.notify("Tool display preset reset to opencode.", "info");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalized.startsWith("preset ")) {
|
||||
const candidate = normalized.slice("preset ".length).trim();
|
||||
const preset = parseToolDisplayPreset(candidate);
|
||||
if (!preset) {
|
||||
ctx.ui.notify(`Unknown preset. Use: /tool-display preset ${PRESET_COMMAND_HINT}`, "warning");
|
||||
return true;
|
||||
}
|
||||
|
||||
controller.setConfig(getToolDisplayPresetConfig(preset), ctx);
|
||||
ctx.ui.notify(`Tool display preset set to ${preset}.`, "info");
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.ui.notify(`Usage: /tool-display [show|reset|preset ${PRESET_COMMAND_HINT}]`, "warning");
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function runToolDisplayCommandHandler(
|
||||
args: string,
|
||||
ctx: ExtensionCommandContext,
|
||||
controller: ToolDisplayConfigController,
|
||||
): Promise<void> {
|
||||
if (handleToolDisplayArgs(args, ctx, controller)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("/tool-display requires interactive TUI mode.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
await openSettingsModal(ctx, controller);
|
||||
}
|
||||
|
||||
export function registerToolDisplayCommand(pi: ExtensionAPI, controller: ToolDisplayConfigController): void {
|
||||
pi.registerCommand("tool-display", {
|
||||
description: "Configure tool output rendering (OpenCode-style)",
|
||||
handler: async (args, ctx) => {
|
||||
await runToolDisplayCommandHandler(args, ctx, controller);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user