import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import type { SettingItem } from "@earendil-works/pi-tui"; import { toOnOff } from "./boolean-format.js"; import type { RtkIntegrationController } from "./command-register.js"; import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js"; import { DEFAULT_RTK_INTEGRATION_CONFIG, RTK_SOURCE_FILTER_LEVELS, type RtkIntegrationConfig, type RuntimeStatus, } from "./types.js"; interface SettingValueSyncTarget { updateValue(id: string, value: string): void; } const ON_OFF = ["on", "off"]; const MODE_VALUES = ["rewrite", "suggest"]; const SOURCE_FILTER_VALUES = [...RTK_SOURCE_FILTER_LEVELS]; const TRUNCATE_MAX_CHAR_VALUES = ["4000", "8000", "12000", "20000", "50000", "100000", "200000"]; const SMART_TRUNCATE_LINE_VALUES = ["40", "80", "120", "160", "220", "320", "500", "1000", "2000", "4000"]; const RTK_USAGE_TEXT = "Usage: /rtk [show|path|verify|stats|clear-stats|reset|help] (or run /rtk with no args to open settings modal)"; const SETTINGS_TAB_DEFINITIONS = [ { label: "General", settingIds: ["enabled", "mode", "showRewriteNotifications", "guardWhenRtkMissing"], }, { label: "Compaction", settingIds: [ "outputCompactionEnabled", "outputStripAnsi", "outputAggregateTestOutput", "outputFilterBuildOutput", "outputCompactGitOutput", "outputAggregateLinterOutput", "outputGroupSearchOutput", "outputTrackSavings", ], }, { label: "Read & Source", settingIds: [ "outputReadCompactionEnabled", "outputSourceFilteringEnabled", "outputSourceFiltering", "outputPreserveExactSkillReads", ], }, { label: "Truncation", settingIds: [ "outputTruncateEnabled", "outputTruncateMaxChars", "outputSmartTruncate", "outputSmartTruncateMaxLines", ], }, ] as const; function buildTabbedSettingGroups(settings: SettingItem[]): Array<{ label: string; settings: SettingItem[] }> { const byId = new Map(settings.map((setting) => [setting.id, setting])); const assignedIds = new Set(); const tabs = SETTINGS_TAB_DEFINITIONS.map(({ label, settingIds }) => ({ label, settings: settingIds.map((id) => { const setting = byId.get(id); if (!setting) { throw new Error(`Missing setting item for tab '${label}': ${id}`); } if (assignedIds.has(id)) { throw new Error(`Setting item assigned to multiple tabs: ${id}`); } assignedIds.add(id); return setting; }), })); const unassignedIds = settings.map((setting) => setting.id).filter((id) => !assignedIds.has(id)); if (unassignedIds.length > 0) { throw new Error(`Unassigned setting items: ${unassignedIds.join(", ")}`); } return tabs; } function parseSourceFilterLevel( value: string, ): RtkIntegrationConfig["outputCompaction"]["sourceCodeFiltering"] | undefined { return SOURCE_FILTER_VALUES.includes(value as (typeof SOURCE_FILTER_VALUES)[number]) ? (value as RtkIntegrationConfig["outputCompaction"]["sourceCodeFiltering"]) : undefined; } function parseIntegerInRange(value: string, min: number, max: number): number | undefined { if (!/^\d+$/.test(value)) { return undefined; } const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed < min || parsed > max) { return undefined; } return parsed; } function summarizeRuntimeStatus(runtimeStatus: RuntimeStatus): string { const runtime = runtimeStatus.rtkAvailable ? "rtk=available" : `rtk=missing${runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : ""}`; const executable = runtimeStatus.rtkExecutablePath ? `, rtkPath=${runtimeStatus.rtkExecutablePath}` : runtimeStatus.rtkExecutableResolutionWarning ? `, rtkPath=unresolved (${runtimeStatus.rtkExecutableResolutionWarning})` : ""; return `${runtime}${executable}`; } function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeStatus): string { return `enabled=${config.enabled}, commandRewriting=${config.commandRewritingEnabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, readCompaction=${config.outputCompaction.readCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${summarizeRuntimeStatus(runtimeStatus)}`; } function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] { return [ { id: "enabled", label: "RTK integration enabled", description: "Master switch for rewrite, suggestions, and output compaction", currentValue: toOnOff(config.enabled), values: ON_OFF, }, { id: "commandRewritingEnabled", label: "RTK command rewriting", description: "Optional RTK CLI command rewriting; off keeps FFF in full control of search", currentValue: toOnOff(config.commandRewritingEnabled), values: ON_OFF, }, { id: "mode", label: "Rewrite mode", description: "rewrite = auto-rewrite bash commands, suggest = notify only", currentValue: config.mode, values: MODE_VALUES, }, { id: "showRewriteNotifications", label: "Show rewrite notifications", description: "Show 'RTK rewrite: old -> new' notice in TUI", currentValue: toOnOff(config.showRewriteNotifications), values: ON_OFF, }, { id: "guardWhenRtkMissing", label: "Guard when rtk missing", description: "If on, raw commands run unchanged when rtk binary is unavailable", currentValue: toOnOff(config.guardWhenRtkMissing), values: ON_OFF, }, { id: "outputCompactionEnabled", label: "Output compaction enabled", description: "Compact bash/read tool results to reduce token usage; search results are untouched", currentValue: toOnOff(config.outputCompaction.enabled), values: ON_OFF, }, { id: "outputStripAnsi", label: "Strip ANSI in output", description: "Remove color/control codes from tool output before further compaction", currentValue: toOnOff(config.outputCompaction.stripAnsi), values: ON_OFF, }, { id: "outputReadCompactionEnabled", label: "Read compaction enabled", description: "If off, read tool output stays exact; build/test/git/grep compaction can still run", currentValue: toOnOff(config.outputCompaction.readCompaction.enabled), values: ON_OFF, }, { id: "outputTruncateEnabled", label: "Hard truncation enabled", description: "Apply max character cap after other compaction techniques", currentValue: toOnOff(config.outputCompaction.truncate.enabled), values: ON_OFF, }, { id: "outputTruncateMaxChars", label: "Hard truncation max chars", description: "Maximum characters kept when hard truncation is enabled", currentValue: String(config.outputCompaction.truncate.maxChars), values: TRUNCATE_MAX_CHAR_VALUES, }, { id: "outputSourceFilteringEnabled", label: "Read source filtering enabled", description: "If off, read output skips source-code filtering regardless of selected level", currentValue: toOnOff(config.outputCompaction.sourceCodeFilteringEnabled), values: ON_OFF, }, { id: "outputPreserveExactSkillReads", label: "Preserve exact skill reads", description: "If on, read results under the global Pi skills directory (default: ~/.pi/agent/skills, respects PI_CODING_AGENT_DIR), ~/.agents/skills, .pi/skills, and ancestor .agents/skills skip read compaction", currentValue: toOnOff(config.outputCompaction.preserveExactSkillReads), values: ON_OFF, }, { id: "outputSourceFiltering", label: "Read source filtering", description: "none|minimal|aggressive for read output compaction", currentValue: config.outputCompaction.sourceCodeFiltering, values: SOURCE_FILTER_VALUES, }, { id: "outputSmartTruncate", label: "Read smart truncation", description: "Keep signatures/imports when read output has many lines", currentValue: toOnOff(config.outputCompaction.smartTruncate.enabled), values: ON_OFF, }, { id: "outputSmartTruncateMaxLines", label: "Read smart truncation max lines", description: "Target max lines for smart truncation in read outputs", currentValue: String(config.outputCompaction.smartTruncate.maxLines), values: SMART_TRUNCATE_LINE_VALUES, }, { id: "outputAggregateTestOutput", label: "Aggregate test output", description: "Summarize test command output to failures and key totals", currentValue: toOnOff(config.outputCompaction.aggregateTestOutput), values: ON_OFF, }, { id: "outputFilterBuildOutput", label: "Filter build output", description: "Reduce build noise and keep key error/warning lines", currentValue: toOnOff(config.outputCompaction.filterBuildOutput), values: ON_OFF, }, { id: "outputCompactGitOutput", label: "Compact git output", description: "Condense git command output for lower token usage", currentValue: toOnOff(config.outputCompaction.compactGitOutput), values: ON_OFF, }, { id: "outputAggregateLinterOutput", label: "Aggregate linter output", description: "Summarize linter output by file and issue type", currentValue: toOnOff(config.outputCompaction.aggregateLinterOutput), values: ON_OFF, }, { id: "outputTrackSavings", label: "Track output savings", description: "Collect in-session compaction metrics for /rtk stats", currentValue: toOnOff(config.outputCompaction.trackSavings), values: ON_OFF, }, ]; } function applySetting(config: RtkIntegrationConfig, id: string, value: string): RtkIntegrationConfig { switch (id) { case "enabled": return { ...config, enabled: value === "on" }; case "commandRewritingEnabled": return { ...config, commandRewritingEnabled: value === "on" }; case "mode": return { ...config, mode: value === "suggest" ? "suggest" : "rewrite" }; case "showRewriteNotifications": return { ...config, showRewriteNotifications: value === "on" }; case "guardWhenRtkMissing": return { ...config, guardWhenRtkMissing: value === "on" }; case "outputCompactionEnabled": return { ...config, outputCompaction: { ...config.outputCompaction, enabled: value === "on" }, }; case "outputStripAnsi": return { ...config, outputCompaction: { ...config.outputCompaction, stripAnsi: value === "on" }, }; case "outputReadCompactionEnabled": return { ...config, outputCompaction: { ...config.outputCompaction, readCompaction: { enabled: value === "on" }, }, }; case "outputTruncateEnabled": return { ...config, outputCompaction: { ...config.outputCompaction, truncate: { ...config.outputCompaction.truncate, enabled: value === "on", }, }, }; case "outputTruncateMaxChars": { const parsed = parseIntegerInRange(value, 1_000, 200_000); return { ...config, outputCompaction: { ...config.outputCompaction, truncate: { ...config.outputCompaction.truncate, maxChars: parsed ?? config.outputCompaction.truncate.maxChars, }, }, }; } case "outputSourceFilteringEnabled": return { ...config, outputCompaction: { ...config.outputCompaction, sourceCodeFilteringEnabled: value === "on", }, }; case "outputPreserveExactSkillReads": return { ...config, outputCompaction: { ...config.outputCompaction, preserveExactSkillReads: value === "on", }, }; case "outputSourceFiltering": { const parsedValue = parseSourceFilterLevel(value); return { ...config, outputCompaction: { ...config.outputCompaction, sourceCodeFiltering: parsedValue ?? config.outputCompaction.sourceCodeFiltering, }, }; } case "outputSmartTruncate": return { ...config, outputCompaction: { ...config.outputCompaction, smartTruncate: { ...config.outputCompaction.smartTruncate, enabled: value === "on", }, }, }; case "outputSmartTruncateMaxLines": { const parsed = parseIntegerInRange(value, 40, 4_000); return { ...config, outputCompaction: { ...config.outputCompaction, smartTruncate: { ...config.outputCompaction.smartTruncate, maxLines: parsed ?? config.outputCompaction.smartTruncate.maxLines, }, }, }; } case "outputAggregateTestOutput": return { ...config, outputCompaction: { ...config.outputCompaction, aggregateTestOutput: value === "on", }, }; case "outputFilterBuildOutput": return { ...config, outputCompaction: { ...config.outputCompaction, filterBuildOutput: value === "on", }, }; case "outputCompactGitOutput": return { ...config, outputCompaction: { ...config.outputCompaction, compactGitOutput: value === "on", }, }; case "outputAggregateLinterOutput": return { ...config, outputCompaction: { ...config.outputCompaction, aggregateLinterOutput: value === "on", }, }; case "outputTrackSavings": return { ...config, outputCompaction: { ...config.outputCompaction, trackSavings: value === "on", }, }; default: return config; } } function syncSettingValues(settingsList: SettingValueSyncTarget, config: RtkIntegrationConfig): void { settingsList.updateValue("enabled", toOnOff(config.enabled)); settingsList.updateValue("commandRewritingEnabled", toOnOff(config.commandRewritingEnabled)); settingsList.updateValue("mode", config.mode); settingsList.updateValue("showRewriteNotifications", toOnOff(config.showRewriteNotifications)); settingsList.updateValue("guardWhenRtkMissing", toOnOff(config.guardWhenRtkMissing)); settingsList.updateValue("outputCompactionEnabled", toOnOff(config.outputCompaction.enabled)); settingsList.updateValue("outputStripAnsi", toOnOff(config.outputCompaction.stripAnsi)); settingsList.updateValue("outputReadCompactionEnabled", toOnOff(config.outputCompaction.readCompaction.enabled)); settingsList.updateValue("outputTruncateEnabled", toOnOff(config.outputCompaction.truncate.enabled)); settingsList.updateValue("outputTruncateMaxChars", String(config.outputCompaction.truncate.maxChars)); settingsList.updateValue("outputSourceFilteringEnabled", toOnOff(config.outputCompaction.sourceCodeFilteringEnabled)); settingsList.updateValue("outputPreserveExactSkillReads", toOnOff(config.outputCompaction.preserveExactSkillReads)); settingsList.updateValue("outputSourceFiltering", config.outputCompaction.sourceCodeFiltering); settingsList.updateValue("outputSmartTruncate", toOnOff(config.outputCompaction.smartTruncate.enabled)); settingsList.updateValue("outputSmartTruncateMaxLines", String(config.outputCompaction.smartTruncate.maxLines)); settingsList.updateValue("outputAggregateTestOutput", toOnOff(config.outputCompaction.aggregateTestOutput)); settingsList.updateValue("outputFilterBuildOutput", toOnOff(config.outputCompaction.filterBuildOutput)); settingsList.updateValue("outputCompactGitOutput", toOnOff(config.outputCompaction.compactGitOutput)); settingsList.updateValue("outputAggregateLinterOutput", toOnOff(config.outputCompaction.aggregateLinterOutput)); settingsList.updateValue("outputTrackSavings", toOnOff(config.outputCompaction.trackSavings)); } async function openSettingsModal(ctx: ExtensionCommandContext, controller: RtkIntegrationController): Promise { const overlayOptions = { anchor: "center" as const, width: 86, maxHeight: "85%" as const, margin: 1 }; await ctx.ui.custom( (tui, theme, _keybindings, done) => { let current = controller.getConfig(); let settingsModal: ZellijSettingsModal | null = null; const allSettings = buildSettingItems(current); const tabs = buildTabbedSettingGroups(allSettings); settingsModal = new ZellijSettingsModal( { title: "Pi RTK Optimizer", tabs, activeTabIndex: 0, onChange: (id, newValue) => { current = applySetting(current, id, newValue); controller.setConfig(current, ctx); current = controller.getConfig(); if (settingsModal) { syncSettingValues(settingsModal, current); } }, onClose: () => done(), helpText: `Config: ${controller.getConfigPath()}`, enableSearch: true, }, theme, ); const modal = new ZellijModal( settingsModal, { borderStyle: "rounded", titleBar: { left: "Pi RTK Optimizer", }, helpUndertitle: { variants: [ "←/→ tabs • Type to search • Enter/Space change • Esc close", "←/→ tabs • Type to search • Esc close", "←/→ tabs • Esc close", ], color: "dim", }, overlay: overlayOptions, }, theme, ); return { render(width: number) { return modal.renderModal(width).lines; }, invalidate() { modal.invalidate(); }, handleInput(data: string) { modal.handleInput(data); tui.requestRender(); }, }; }, { overlay: true, overlayOptions }, ); } async function handleArgs( args: string, ctx: ExtensionCommandContext, controller: RtkIntegrationController, ): Promise { const normalized = (args ?? "").trim().toLowerCase(); if (!normalized) { return false; } if (normalized === "help") { ctx.ui.notify(RTK_USAGE_TEXT, "info"); return true; } if (normalized === "show") { ctx.ui.notify(`rtk: ${summarizeConfig(controller.getConfig(), controller.getRuntimeStatus())}`, "info"); return true; } if (normalized === "path") { ctx.ui.notify(`rtk config: ${controller.getConfigPath()}`, "info"); return true; } if (normalized === "verify") { const runtimeStatus = await controller.refreshRuntimeStatus(); if (runtimeStatus.rtkAvailable) { const pathDetail = runtimeStatus.rtkExecutablePath ? ` at ${runtimeStatus.rtkExecutablePath}` : ""; ctx.ui.notify(`RTK binary is available${pathDetail}.`, "info"); } else { ctx.ui.notify( `RTK binary is not available${runtimeStatus.lastError ? `: ${runtimeStatus.lastError}` : ""}.`, "warning", ); } return true; } if (normalized === "stats") { ctx.ui.notify(controller.getMetricsSummary(), "info"); return true; } if (normalized === "clear-stats") { controller.clearMetrics(); ctx.ui.notify("RTK metrics cleared.", "info"); return true; } if (normalized === "reset") { controller.setConfig({ ...DEFAULT_RTK_INTEGRATION_CONFIG }, ctx); ctx.ui.notify("RTK integration settings reset to defaults.", "info"); return true; } ctx.ui.notify(RTK_USAGE_TEXT, "warning"); return true; } export async function handleRtkIntegrationCommand( args: string, ctx: ExtensionCommandContext, controller: RtkIntegrationController, ): Promise { if (await handleArgs(args, ctx, controller)) { return; } if (!ctx.hasUI) { ctx.ui.notify("/rtk requires interactive TUI mode.", "warning"); return; } await openSettingsModal(ctx, controller); }