/**
 * XTRM UI Extension
 *
 * Wraps pi-dex functionality with XTRM-specific preferences:
 * - Uses pi-dex themes and header
 * - Disables pi-dex footer (let custom-footer handle it)
 * - Provides /xtrm-ui commands for theme/density switching
 *
 * This eliminates the race condition between pi-dex's footer and
 * XTRM's custom-footer extension.
 */

import type {
  BashToolDetails,
  EditToolDetails,
  ExtensionAPI,
  ExtensionContext,
  FindToolDetails,
  GrepToolDetails,
  LsToolDetails,
  ReadToolDetails,
  ToolResultEvent,
} from "@mariozechner/pi-coding-agent";
import {
  CustomEditor,
  createBashTool,
  createEditTool,
  createFindTool,
  createGrepTool,
  createLsTool,
  createReadTool,
  createWriteTool,
} from "@mariozechner/pi-coding-agent";
import { Box, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
  cleanOutputLines,
  countPrefixedItems,
  createUnifiedLineDiff,
  diffStats,
  formatDuration,
  formatLineLabel,
  formatPayloadSize,
  joinCompactMeta,
  joinMeta,
  lineCount,
  previewLines,
  renderRichDiffPreview,
  renderToolSummary,
  TOOL_ROW_MARKER,
  shortenCommand,
  shortenPath,
} from "./format";

// ============================================================================
// Types
// ============================================================================

export type XtrmThemeName = "pidex-dark" | "pidex-light" | "pidex-dark-flattools" | "pidex-light-flattools";
export type XtrmDensity = "compact" | "comfortable";
export type XtrmExternalToolChrome = "background" | "box";

export interface XtrmUiPrefs {
  themeName: XtrmThemeName;
  density: XtrmDensity;
  showHeader: boolean;
  compactTools: boolean;
  showFooter: boolean; // Our key addition - when false, skip setFooter()
  forceTheme: boolean; // When false, skip setTheme (allow external theme override)
  toolRowBg: boolean; // Subtle background behind tool text rows (no padding)
  compactExternalToolResults: boolean; // Compact extension tool results (disables full expand output)
  externalToolChrome: XtrmExternalToolChrome; // Visual treatment for non-native tool rows
  hideThinkingPlaceholder: boolean; // When false, hidden thinking blocks render no placeholder text
}

// ============================================================================
// Defaults
// ============================================================================

export const XTRM_UI_PREFS_ENTRY = "xtrm-ui-prefs";

export const DEFAULT_PREFS: XtrmUiPrefs = {
  themeName: "pidex-dark",
  density: "compact",
  showHeader: true,
  compactTools: true,
  showFooter: false, // XTRM: disable pi-dex footer, use custom-footer
  forceTheme: true,
  toolRowBg: false,
  compactExternalToolResults: true,
  externalToolChrome: "background",
  hideThinkingPlaceholder: false,
};

let activeExternalToolChrome: XtrmExternalToolChrome = DEFAULT_PREFS.externalToolChrome;

function setActiveExternalToolChrome(chrome: XtrmExternalToolChrome): void {
  activeExternalToolChrome = chrome;
}


// ============================================================================
// Preferences
// ============================================================================

type MaybeCustomEntry = {
  type?: string;
  customType?: string;
  data?: unknown;
};

function normalizePrefs(input: unknown): XtrmUiPrefs {
  if (!input || typeof input !== "object") return { ...DEFAULT_PREFS };
  const source = input as Partial<XtrmUiPrefs>;
  return {
    themeName: source.themeName === "pidex-light" || source.themeName === "pidex-light-flattools" ? "pidex-light" : "pidex-dark",
    density: source.density === "comfortable" ? "comfortable" : "compact",
    showHeader: source.showHeader ?? DEFAULT_PREFS.showHeader,
    compactTools: source.compactTools ?? DEFAULT_PREFS.compactTools,
    showFooter: source.showFooter ?? DEFAULT_PREFS.showFooter,
    forceTheme: source.forceTheme ?? DEFAULT_PREFS.forceTheme,
    toolRowBg: source.toolRowBg ?? DEFAULT_PREFS.toolRowBg,
    compactExternalToolResults:
      source.compactExternalToolResults ?? DEFAULT_PREFS.compactExternalToolResults,
    externalToolChrome: source.externalToolChrome === "box" ? "box" : "background",
    hideThinkingPlaceholder: source.hideThinkingPlaceholder ?? DEFAULT_PREFS.hideThinkingPlaceholder,
  };
}

function loadPrefs(entries: ReadonlyArray<MaybeCustomEntry>): XtrmUiPrefs {
  for (let i = entries.length - 1; i >= 0; i--) {
    const entry = entries[i];
    if (entry?.type === "custom" && entry.customType === XTRM_UI_PREFS_ENTRY) {
      return normalizePrefs(entry.data);
    }
  }
  return { ...DEFAULT_PREFS };
}

function persistPrefs(pi: ExtensionAPI, prefs: XtrmUiPrefs): void {
  pi.appendEntry(XTRM_UI_PREFS_ENTRY, prefs);
}


// ============================================================================
// Thinking Chrome
// ============================================================================

type AssistantMessageComponentCtor = {
  prototype: {
    updateContent?: (message: AssistantMessageLike) => void;
    setExpanded?: (expanded: boolean) => void;
  };
};

type AssistantContentBlock = { type?: string; thinking?: string };
type AssistantMessageLike = { content?: AssistantContentBlock[] };
type PatchableAssistantMessage = {
  hideThinkingBlock?: boolean;
  hiddenThinkingLabel?: string;
  lastMessage?: AssistantMessageLike;
  __xtrmThinkingExpanded?: boolean;
  updateContent?: (message: AssistantMessageLike) => void;
};

const PATCHED_ASSISTANT_MESSAGE = "__xtrmUiSilentHiddenThinking";

function maybeFileUrlToPath(value: string): string {
  return value.startsWith("file:") ? fileURLToPath(value) : value;
}

function resolvePiCodingAgentEntryPath(): string {
  const candidates: string[] = [];

  const argvPath = process.argv[1];
  if (argvPath && existsSync(argvPath)) {
    const realArgvPath = realpathSync(argvPath);
    if (realArgvPath.endsWith("/dist/cli.js")) {
      candidates.push(join(dirname(realArgvPath), "index.js"));
    }
  }

  candidates.push(
    join(dirname(process.execPath), "..", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"),
    join(dirname(process.execPath), "..", "lib", "node_modules", "@mariozechner", "pi-coding-agent", "dist", "index.js"),
  );

  for (const packageName of ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"]) {
    try {
      candidates.push(maybeFileUrlToPath(import.meta.resolve(packageName)));
    } catch {}
  }

  const entryPath = candidates.find((candidate) => existsSync(candidate));
  if (!entryPath) throw new Error("Could not resolve pi-coding-agent entry path");
  return entryPath;
}

async function installSilentHiddenThinkingPatch(): Promise<void> {
  const entryPath = resolvePiCodingAgentEntryPath();
  const componentPath = join(dirname(entryPath), "modes", "interactive", "components", "assistant-message.js");
  const mod = await import(pathToFileURL(componentPath).href) as {
    AssistantMessageComponent?: AssistantMessageComponentCtor;
  };
  const proto = mod.AssistantMessageComponent?.prototype as
    | (AssistantMessageComponentCtor["prototype"] & { [PATCHED_ASSISTANT_MESSAGE]?: boolean })
    | undefined;
  if (!proto?.updateContent || proto[PATCHED_ASSISTANT_MESSAGE]) return;

  const updateContent = proto.updateContent;
  proto.updateContent = function patchedUpdateContent(this: PatchableAssistantMessage, message: AssistantMessageLike) {
    if (this.hiddenThinkingLabel === "" && Array.isArray(message.content)) {
      if (!this.__xtrmThinkingExpanded) {
        updateContent.call(this, {
          ...message,
          content: message.content.filter((block) => block.type !== "thinking" || !block.thinking?.trim()),
        });
        return;
      }

      const previousHideThinking = this.hideThinkingBlock;
      this.hideThinkingBlock = false;
      updateContent.call(this, message);
      this.hideThinkingBlock = previousHideThinking;
      return;
    }
    updateContent.call(this, message);
  };

  proto.setExpanded = function setExpanded(this: PatchableAssistantMessage, expanded: boolean) {
    this.__xtrmThinkingExpanded = expanded;
    if (this.lastMessage) this.updateContent?.(this.lastMessage);
  };
  proto[PATCHED_ASSISTANT_MESSAGE] = true;
}

type ToolExecutionComponentCtor = {
  prototype: {
    getRenderShell?: () => "default" | "self";
    hasRendererDefinition?: () => boolean;
    render?: (width: number) => string[];
  };
};

type PatchableToolExecutionComponent = {
  toolName?: string;
  args?: unknown;
  result?: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean };
  expanded?: boolean;
  hasRendererDefinition?: () => boolean;
};

type ExternalToolFrameKind = "serena" | "gitnexus" | "structured" | "process" | "external";

const PATCHED_EXTERNAL_TOOL_FRAME = "__xtrmUiExternalToolFrame";
const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 12;
const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;

function stripAnsi(text: string): string {
  return text.replace(ANSI_PATTERN, "");
}

function isBlankRenderedLine(line: string): boolean {
  return stripAnsi(line).trim().length === 0;
}

function externalToolFrameKind(toolName: string | undefined): ExternalToolFrameKind | undefined {
  if (!toolName || XTRM_BUILTIN_TOOLS.has(toolName)) return undefined;
  if (toolName === "structured_return") return "structured";
  if (toolName === "process") return "process";
  if (toolName.startsWith("gitnexus_")) return "gitnexus";
  if (SERENA_COMPACT_TOOLS.has(toolName)) return "serena";
  return "external";
}

function padVisible(text: string, width: number): string {
  const visible = visibleWidth(text);
  return text + " ".repeat(Math.max(0, width - visible));
}

function getXtrmOriginalText(details: unknown): string | undefined {
  const record = asRecord(details);
  return typeof record?.xtrmOriginalText === "string" ? record.xtrmOriginalText : undefined;
}

function getToolArgs(component: PatchableToolExecutionComponent): Record<string, unknown> {
  return component.args && typeof component.args === "object" && !Array.isArray(component.args)
    ? component.args as Record<string, unknown>
    : {};
}

function summarizeExternalToolPending(toolName: string | undefined, input: Record<string, unknown>): string {
  const name = toolName ?? "tool";
  if (name === "structured_return") {
    return `${TOOL_ROW_MARKER} structured_return ${shortenCommand(String(input.command ?? "running"), 38)}`;
  }
  if (name === "process") {
    return `${TOOL_ROW_MARKER} process ${String(input.action ?? "running")}`;
  }
  if (name.startsWith("gitnexus_")) {
    const subject = summarizeSerenaSubject(name, input) ?? summarizeToolSubject(name, input);
    return `${TOOL_ROW_MARKER} ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
  }
  if (SERENA_COMPACT_TOOLS.has(name)) {
    const subject = summarizeSerenaSubject(name, input);
    return `${TOOL_ROW_MARKER} serena ${name}${subject ? ` ${subject}` : ""}`;
  }
  const subject = summarizeToolSubject(name, input) ?? summarizeSerenaSubject(name, input);
  return `${TOOL_ROW_MARKER} ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
}

function extractResultTextLines(component: PatchableToolExecutionComponent): string[] | undefined {
  const originalText = component.expanded ? getXtrmOriginalText(component.result?.details) : undefined;
  if (originalText) return originalText.split("\n");

  const text = component.result?.content?.find((content) => content.type === "text")?.text;
  if (text) return text.split("\n");

  return [summarizeExternalToolPending(component.toolName, getToolArgs(component))];
}

function trimRenderedToolLines(lines: string[]): string[] {
  let start = 0;
  let end = lines.length;
  while (start < end && isBlankRenderedLine(lines[start] ?? "")) start++;
  while (end > start && isBlankRenderedLine(lines[end - 1] ?? "")) end--;
  return lines.slice(start, end).map((line) => line.replace(/\s+$/u, ""));
}

function externalToolBadgeColor(kind: ExternalToolFrameKind, text: string): string {
  const bgColors: Record<ExternalToolFrameKind, [number, number, number]> = {
    serena: [82, 210, 255],
    gitnexus: [178, 154, 255],
    structured: [205, 166, 255],
    process: [92, 226, 255],
    external: [178, 190, 210],
  };
  const [badgeR, badgeG, badgeB] = bgColors[kind];
  return `\x1b[38;2;3;8;12m\x1b[48;2;${badgeR};${badgeG};${badgeB}m${text}\x1b[39m\x1b[49m`;
}

function highlightExternalToolBadge(kind: ExternalToolFrameKind, line: string): string {
  const match = line.match(/^([•›]\s+)(\S+)/u);
  if (!match?.[1] || !match[2]) return line;
  return match[1] + externalToolBadgeColor(kind, match[2]) + line.slice(match[1].length + match[2].length);
}

function externalToolBorderColor(kind: ExternalToolFrameKind, text: string): string {
  const colors: Record<ExternalToolFrameKind, [number, number, number]> = {
    serena: [150, 210, 255],
    gitnexus: [185, 168, 255],
    structured: [205, 166, 255],
    process: [145, 231, 255],
    external: [168, 181, 199],
  };
  const [r, g, b] = colors[kind];
  return `[2m[38;2;${r};${g};${b}m${text}[39m[22m`;
}

function collapsedExternalToolLines(contentLines: string[], expanded: boolean): string[] {
  return expanded ? contentLines : [contentLines.join(" · ")];
}

function renderExternalToolBackgroundLines(
  contentLines: string[],
  width: number,
  kind: ExternalToolFrameKind,
  expanded: boolean,
): string[] {
  const availableWidth = Math.max(8, width);
  const renderWidth = availableWidth;
  const visibleLines = collapsedExternalToolLines(contentLines, expanded);

  return visibleLines.map((rawLine) => {
    const line = truncateToWidth(rawLine, Math.max(1, renderWidth));
    return highlightExternalToolBadge(kind, line);
  });
}

function renderExternalToolBoxLines(
  contentLines: string[],
  width: number,
  kind: ExternalToolFrameKind,
  expanded: boolean,
): string[] {
  const availableWidth = Math.max(8, width - 4);
  const maxContentWidth = expanded ? availableWidth : Math.min(availableWidth, 34);
  const visibleLines = collapsedExternalToolLines(contentLines, expanded);
  const contentWidth = Math.max(
    1,
    Math.min(maxContentWidth, ...visibleLines.map((line) => visibleWidth(line))),
  );
  const innerWidth = contentWidth + 2;

  const framed = [externalToolBorderColor(kind, `╭${"─".repeat(innerWidth)}╮`)];
  for (const rawLine of visibleLines) {
    const line = truncateToWidth(rawLine, contentWidth);
    framed.push(`${externalToolBorderColor(kind, "│")} ${padVisible(line, contentWidth)} ${externalToolBorderColor(kind, "│")}`);
  }
  framed.push(externalToolBorderColor(kind, `╰${"─".repeat(innerWidth)}╯`));
  return framed;
}

function renderExternalToolLines(
  lines: string[],
  width: number,
  kind: ExternalToolFrameKind,
  expanded = false,
): string[] {
  const contentLines = trimRenderedToolLines(lines).filter((line) => !isBlankRenderedLine(line));
  if (contentLines.length === 0) return [];

  return activeExternalToolChrome === "box"
    ? renderExternalToolBoxLines(contentLines, width, kind, expanded)
    : renderExternalToolBackgroundLines(contentLines, width, kind, expanded);
}

async function installExternalToolFramePatch(): Promise<void> {
  const entryPath = resolvePiCodingAgentEntryPath();
  const componentPath = join(dirname(entryPath), "modes", "interactive", "components", "tool-execution.js");
  const mod = await import(pathToFileURL(componentPath).href) as {
    ToolExecutionComponent?: ToolExecutionComponentCtor;
  };
  const proto = mod.ToolExecutionComponent?.prototype as
    | (ToolExecutionComponentCtor["prototype"] & { [PATCHED_EXTERNAL_TOOL_FRAME]?: boolean })
    | undefined;
  if (!proto?.render || proto[PATCHED_EXTERNAL_TOOL_FRAME] === EXTERNAL_TOOL_FRAME_PATCH_VERSION) return;

  const getRenderShell = proto.getRenderShell;
  const render = proto.render;

  proto.getRenderShell = function patchedGetRenderShell(this: PatchableToolExecutionComponent) {
    const kind = externalToolFrameKind(this.toolName);
    if (kind) return "self";
    return getRenderShell?.call(this) ?? "default";
  };

  proto.render = function patchedRender(this: PatchableToolExecutionComponent, width: number) {
    const rendered = render.call(this, width);
    const kind = externalToolFrameKind(this.toolName);
    if (!kind || rendered.length === 0) return rendered;

    const firstContentIndex = rendered.findIndex((line) => !isBlankRenderedLine(line));
    const leading = firstContentIndex > 0 ? rendered.slice(0, firstContentIndex) : [];
    const content = extractResultTextLines(this) ?? rendered;
    const styled = renderExternalToolLines(content, width, kind, Boolean(this.expanded));
    return styled.length > 0 ? [...leading, ...styled] : rendered;
  };

  proto[PATCHED_EXTERNAL_TOOL_FRAME] = EXTERNAL_TOOL_FRAME_PATCH_VERSION;
}

function applyThinkingChrome(ctx: ExtensionContext, prefs: XtrmUiPrefs): void {
  (ctx.ui as { setHiddenThinkingLabel?: (label?: string) => void }).setHiddenThinkingLabel?.(
    prefs.hideThinkingPlaceholder ? undefined : "",
  );
}

// ============================================================================
// Chrome Application
// ============================================================================

function fitVisible(text: string, width: number): string {
  const truncated = truncateToWidth(text, width);
  return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
}

function resolveThemeForPrefs(prefs: XtrmUiPrefs): XtrmThemeName {
  const base = prefs.themeName === "pidex-light" || prefs.themeName === "pidex-light-flattools" ? "pidex-light" : "pidex-dark";
  if (!prefs.toolRowBg) return (base + "-flattools") as XtrmThemeName;
  return base as XtrmThemeName;
}

function formatThinking(level: string): string {
  return level === "off" ? "standard" : level;
}

function applyXtrmChrome(
  ctx: ExtensionContext,
  prefs: XtrmUiPrefs,
  getThinkingLevel: () => string
): void {
  // Theme
  ctx.ui.setTheme(resolveThemeForPrefs(prefs));

  // Tool expansion
  ctx.ui.setToolsExpanded(!prefs.compactTools);

  // Editor — density-aware input padding
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
    const editor = new XtrmEditor(tui, theme, keybindings);
    editor.setPrefs(prefs);
    return editor;
  });

  // Header (optional)
  if (prefs.showHeader) {
    ctx.ui.setHeader((_tui, theme) => ({
      invalidate() {},
      render(width: number): string[] {
        const boxWidth = width >= 54 ? 50 : Math.max(24, width);
        const model = ctx.model?.id ?? "no-model";
        const thinking = getThinkingLevel();
        const border = (text: string) => theme.fg("borderAccent", text);
        const leftPad = "";

        const top = leftPad + border(`╭${"─".repeat(Math.max(0, boxWidth - 2))}╮`);
        const line1 =
          leftPad +
          border("│") +
          fitVisible(
            ` ${theme.fg("dim", ">_")} ${theme.bold("XTRM")} ${theme.fg("dim", `(v1.0.0)`)}`,
            boxWidth - 2
          ) +
          border("│");
        const gap = leftPad + border("│") + fitVisible("", boxWidth - 2) + border("│");
        const line2 =
          leftPad +
          border("│") +
          fitVisible(
            ` ${theme.fg("dim", "model:".padEnd(11))}${model} ${thinking}${theme.fg("accent", "    /model")}${theme.fg("dim", " to change")}`,
            boxWidth - 2
          ) +
          border("│");
        const line3 =
          leftPad +
          border("│") +
          fitVisible(
            ` ${theme.fg("dim", "directory:".padEnd(11))}${basename(ctx.cwd)}`,
            boxWidth - 2
          ) +
          border("│");
        const bottom = leftPad + border(`╰${"─".repeat(Math.max(0, boxWidth - 2))}╯`);

        return [top, line1, gap, line2, line3, bottom];
      },
    }));
  } else {
    ctx.ui.setHeader(undefined);
  }

  // Footer - ONLY if showFooter is true (default false for XTRM)
  // This is the key difference from pi-dex - we let custom-footer handle it
  if (prefs.showFooter) {
    ctx.ui.setFooter((tui, theme, footerData) => {
      const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
      return {
        dispose: unsubscribe,
        invalidate() {},
        render(width: number): string[] {
          const modelId = ctx.model?.id ?? "no-model";
          const thinking = getThinkingLevel();
          const contextUsage = ctx.getContextUsage();
          const leftPct = contextUsage?.percent != null ? `${100 - Math.round(contextUsage.percent)}% left` : undefined;
          const line = theme.fg(
            "dim",
            [`${modelId} ${thinking}`, leftPct, basename(ctx.cwd)]
              .filter(Boolean)
              .join(" · ")
          );
          return [truncateToWidth(line, width)];
        },
      };
    });
  }
  // If showFooter is false, we do NOT call setFooter - custom-footer will handle it
}


function writeExternalCompactFlag(enabled: boolean): void {
  try {
    const settingsPath = join(process.env.HOME ?? "", ".pi", "agent", "settings.json");
    if (!settingsPath) return;
    let settings: Record<string, unknown> = {};
    if (existsSync(settingsPath)) {
      try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch {}
    }
    settings.xtrmExternalCompact = enabled;
    writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
  } catch {}
}

// ============================================================================
// Tool Render Helpers
// ============================================================================

function renderOutputPreview(theme: any, lines: string[], maxLines: number): string {
  const subset = lines.slice(0, maxLines);
  let text = subset.map((line) => theme.fg("toolOutput", `  ${line}`)).join("\n");
  if (lines.length > maxLines) text += `\n${theme.fg("muted", `  … +${lines.length - maxLines} more`)}`;
  return text;
}

function renderVerticalPreview(theme: any, lines: string[], maxLines: number): string {
  const subset = lines.slice(0, maxLines);
  let text = subset.map((line) => `${theme.fg("muted", "│")} ${theme.fg("toolOutput", line)}`).join("\n");
  if (lines.length > maxLines) text += `\n${theme.fg("muted", "│")} ${theme.fg("muted", `… +${lines.length - maxLines} more lines`)}`;
  return text;
}


function lineRange(offset?: number, limit?: number): string | undefined {
  if (offset == null && limit == null) return undefined;
  const start = offset ?? 1;
  if (limit == null) return `${start}`;
  return `${start}-${start + limit - 1}`;
}

function summarizeCount(text: string): number {
  return text.split("\n").filter((line) => line.trim().length > 0).length;
}

// ============================================================================
// Editor (task p38n.3)
// ============================================================================

class XtrmEditor extends CustomEditor {
  constructor(...args: ConstructorParameters<typeof CustomEditor>) {
    super(...args);
  }

  setPrefs(prefs: XtrmUiPrefs): void {
    this.setPaddingX(prefs.density === "comfortable" ? 2 : 1);
  }

  render(width: number): string[] {
    return super.render(width);
  }
}

// ============================================================================
// Commands
// ============================================================================

function sendInfoMessage(pi: ExtensionAPI, title: string, content: string): void {
  pi.sendMessage({
    customType: "xtrm-ui-info",
    content,
    display: true,
    details: { title },
  });
}

function parseThemeArg(arg: string): XtrmThemeName | undefined {
  const normalized = arg.trim().toLowerCase();
  if (normalized === "dark" || normalized === "pidex-dark") return "pidex-dark";
  if (normalized === "light" || normalized === "pidex-light") return "pidex-light";
  return undefined;
}

function parseDensityArg(arg: string): XtrmDensity | undefined {
  const normalized = arg.trim().toLowerCase();
  if (normalized === "compact") return "compact";
  if (normalized === "comfortable" || normalized === "normal") return "comfortable";
  return undefined;
}

function parseExternalToolChromeArg(arg: string): XtrmExternalToolChrome | undefined {
  const normalized = arg.trim().toLowerCase();
  if (normalized === "background" || normalized === "bg" || normalized === "row") return "background";
  if (normalized === "box" || normalized === "frame" || normalized === "border") return "box";
  return undefined;
}

function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPrefs: (p: XtrmUiPrefs) => void, getThinkingLevel: () => string) {
  pi.registerMessageRenderer("xtrm-ui-info", (message, _options, theme) => {
    const title = (message.details as { title?: string } | undefined)?.title ?? "XTRM UI";
    const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
    box.addChild(new Text(theme.fg("customMessageLabel", theme.bold(title)), 0, 0));
    box.addChild(new Text(theme.fg("customMessageText", String(message.content ?? "")), 0, 0));
    return box;
  });

  pi.registerCommand("xtrm-ui", {
    description: "Show XTRM UI status and active preferences",
    handler: async (args, ctx) => {
      const trimmedArgs = args.trim();
      if (trimmedArgs) {
        const [subcommand, ...rest] = trimmedArgs.split(/\s+/u);
        if (subcommand === "chrome" || subcommand === "external-chrome" || subcommand === "tool-chrome") {
          const externalToolChrome = parseExternalToolChromeArg(rest.join(" "));
          if (!externalToolChrome) {
            ctx.ui.notify("Usage: /xtrm-ui chrome background|box", "warning");
            return;
          }
          const prefs = { ...getPrefs(), externalToolChrome };
          setPrefs(prefs);
          persistPrefs(pi, prefs);
          ctx.ui.notify(`External tool chrome set to ${externalToolChrome}.`, "info");
          return;
        }
        ctx.ui.notify("Usage: /xtrm-ui [chrome background|box]", "warning");
        return;
      }

      const prefs = getPrefs();
      const contextUsage = ctx.getContextUsage();
      const lines = [
        `Theme: ${prefs.themeName}`,
        `Force theme: ${prefs.forceTheme ? "on" : "off"}`,
        `Density: ${prefs.density}`,
        `Compact tools: ${prefs.compactTools ? "on" : "off"}`,
        `Show header: ${prefs.showHeader ? "yes" : "no"}`,
        `Show footer: ${prefs.showFooter ? "yes" : "no"} (custom-footer handles this)`,
        `Tool row background: ${prefs.toolRowBg ? "on" : "off"}`,
        `Compact external tool results: ${prefs.compactExternalToolResults ? "on" : "off"}`,
        `External tool chrome: ${prefs.externalToolChrome}`,
        `Model: ${ctx.model?.id ?? "none"}`,
        `Context: ${contextUsage?.tokens ?? "unknown"}/${contextUsage?.contextWindow ?? "unknown"}`,
      ];
      sendInfoMessage(pi, "XTRM UI status", lines.join("\\n"));
    },
  });

  pi.registerCommand("xtrm-ui-theme", {
    description: "Switch XTRM UI theme: dark|light",
    getArgumentCompletions: (prefix) => {
      const values = ["dark", "light"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const themeName = parseThemeArg(args);
      if (!themeName) {
        ctx.ui.notify("Usage: /xtrm-ui-theme dark|light", "warning");
        return;
      }
      const prefs = { ...getPrefs(), themeName };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      applyXtrmChrome(ctx, prefs, getThinkingLevel);
      ctx.ui.notify(`XTRM UI theme set to ${themeName}`, "info");
    },
  });

  pi.registerCommand("xtrm-ui-density", {
    description: "Switch XTRM UI density: compact|comfortable",
    getArgumentCompletions: (prefix) => {
      const values = ["compact", "comfortable"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const density = parseDensityArg(args);
      if (!density) {
        ctx.ui.notify("Usage: /xtrm-ui-density compact|comfortable", "warning");
        return;
      }
      const prefs = { ...getPrefs(), density, compactExternalToolResults: density === "compact" };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      writeExternalCompactFlag(prefs.compactExternalToolResults);
      applyXtrmChrome(ctx, prefs, getThinkingLevel);
      ctx.ui.notify(`XTRM UI density set to ${density}`, "info");
    },
  });

  pi.registerCommand("xtrm-ui-header", {
    description: "Toggle XTRM UI header: on|off",
    getArgumentCompletions: (prefix) => {
      const values = ["on", "off"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const showHeader = args.trim().toLowerCase() === "on";
      const prefs = { ...getPrefs(), showHeader };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      applyXtrmChrome(ctx, prefs, getThinkingLevel);
      ctx.ui.notify(`XTRM UI header ${showHeader ? "enabled" : "disabled"}`, "info");
    },
  });

  pi.registerCommand("xtrm-ui-forcetheme", {
    description: "Control whether xtrm-ui overrides the active theme: on|off",
    getArgumentCompletions: (prefix) => {
      const values = ["on", "off"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const normalized = args.trim().toLowerCase();
      if (normalized !== "on" && normalized !== "off") {
        ctx.ui.notify("Usage: /xtrm-ui-forcetheme on|off", "warning");
        return;
      }
      const forceTheme = normalized === "on";
      const prefs = { ...getPrefs(), forceTheme };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      applyXtrmChrome(ctx, prefs, getThinkingLevel);
      ctx.ui.notify(`XTRM UI force theme ${forceTheme ? "enabled" : "disabled"}`, "info");
    },
  });

  pi.registerCommand("xtrm-ui-rowbg", {
    description: "Toggle subtle tool-row background: on|off",
    getArgumentCompletions: (prefix) => {
      const values = ["on", "off"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const normalized = args.trim().toLowerCase();
      if (normalized !== "on" && normalized !== "off") {
        ctx.ui.notify("Usage: /xtrm-ui-rowbg on|off", "warning");
        return;
      }
      const toolRowBg = normalized === "on";
      const prefs = { ...getPrefs(), toolRowBg };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      applyXtrmChrome(ctx, prefs, getThinkingLevel);
      ctx.ui.notify(`Tool row background ${toolRowBg ? "enabled" : "disabled"}.`, "info");
    },
  });

  pi.registerCommand("xtrm-ui-compact-tools", {
    description: "Compact extension tool results: on|off (off keeps full Ctrl+O expand output)",
    getArgumentCompletions: (prefix) => {
      const values = ["on", "off"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const normalized = args.trim().toLowerCase();
      if (normalized !== "on" && normalized !== "off") {
        ctx.ui.notify("Usage: /xtrm-ui-compact-tools on|off", "warning");
        return;
      }
      const compactExternalToolResults = normalized === "on";
      const prefs = { ...getPrefs(), compactExternalToolResults };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      writeExternalCompactFlag(compactExternalToolResults);
      ctx.ui.notify(
        `Compact external tool results ${compactExternalToolResults ? "enabled" : "disabled"}.`,
        "info",
      );
    },
  });

  pi.registerCommand("xtrm-ui-external-chrome", {
    description: "Choose non-native tool chrome: background|box",
    getArgumentCompletions: (prefix) => {
      const values = ["background", "box"].filter((item) => item.startsWith(prefix));
      return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
    },
    handler: async (args, ctx) => {
      const externalToolChrome = parseExternalToolChromeArg(args);
      if (!externalToolChrome) {
        ctx.ui.notify("Usage: /xtrm-ui-external-chrome background|box", "warning");
        return;
      }
      const prefs = { ...getPrefs(), externalToolChrome };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      ctx.ui.notify(`External tool chrome set to ${externalToolChrome}.`, "info");
    },
  });

  pi.registerCommand("xtrm-ui-reset", {
    description: "Restore XTRM UI defaults",
    handler: async (_args, ctx) => {
      const prefs = { ...DEFAULT_PREFS };
      setPrefs(prefs);
      persistPrefs(pi, prefs);
      applyXtrmChrome(ctx, prefs, getThinkingLevel);
      ctx.ui.notify("XTRM UI reset to defaults", "info");
    },
  });
}

// ============================================================================
// Tool Renderers (ported from pi-dex tooling.ts)
// ============================================================================

type BuiltInTools = ReturnType<typeof createBuiltInTools>;

type XtrmMeta<TArgs = Record<string, unknown>> = {
  tool: string;
  args: TArgs;
  durationMs: number;
};

type XtrmWritePreview =
  | { kind: "created"; lineCount: number }
  | { kind: "updated"; diff: string; additions: number; removals: number }
  | { kind: "unchanged" };

type DetailsWithXtrmMeta<TDetails, TArgs = Record<string, unknown>> = TDetails & {
  xtrmMeta?: XtrmMeta<TArgs>;
  xtrmWritePreview?: XtrmWritePreview;
};

const toolCache = new Map<string, BuiltInTools>();

function createBuiltInTools(cwd: string) {
  return {
    bash: createBashTool(cwd),
    read: createReadTool(cwd),
    edit: createEditTool(cwd),
    write: createWriteTool(cwd),
    find: createFindTool(cwd),
    grep: createGrepTool(cwd),
    ls: createLsTool(cwd),
  };
}

function getTools(cwd: string): BuiltInTools {
  let tools = toolCache.get(cwd);
  if (!tools) {
    tools = createBuiltInTools(cwd);
    toolCache.set(cwd, tools);
  }
  return tools;
}

function withXtrmMeta<TDetails extends object, TArgs extends Record<string, unknown>>(
  details: TDetails | undefined,
  tool: string,
  args: TArgs,
  durationMs: number,
): DetailsWithXtrmMeta<TDetails, TArgs> {
  return { ...(details ?? ({} as TDetails)), xtrmMeta: { tool, args, durationMs } };
}

function getXtrmMeta<TDetails extends object, TArgs extends Record<string, unknown>>(
  details: TDetails | undefined,
): XtrmMeta<TArgs> | undefined {
  if (!details || typeof details !== "object") return undefined;
  return (details as DetailsWithXtrmMeta<TDetails, TArgs>).xtrmMeta;
}

function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
  const item = result.content.find((content) => content.type === "text");
  return item?.text ?? "";
}

function createWritePreview(path: string, nextContent: string): XtrmWritePreview {
  if (!path || !existsSync(path)) {
    return { kind: "created", lineCount: lineCount(nextContent) };
  }

  let currentContent = "";
  try {
    currentContent = readFileSync(path, "utf8");
  } catch {
    return { kind: "created", lineCount: lineCount(nextContent) };
  }

  if (currentContent === nextContent) return { kind: "unchanged" };

  const diff = createUnifiedLineDiff(currentContent, nextContent);
  const stats = diffStats(diff);
  return {
    kind: "updated",
    diff,
    additions: stats.additions,
    removals: stats.removals,
  };
}

function renderPendingCall(toolName: string, args: Record<string, unknown>, theme: any): Text {
  return new Text(renderToolSummary(theme, "pending", toolName, summarizeToolSubject(toolName, args), undefined), 0, 0);
}

function stableToolSignature(toolName: string, args: Record<string, unknown>): string {
  return `${toolName}:${JSON.stringify(args)}`;
}

function summarizeToolSubject(toolName: string, args: Record<string, unknown>): string | undefined {
  switch (toolName) {
    case "bash": return shortenCommand(String(args.command ?? ""), 52);
    case "read": {
      const path = shortenPath(String(args.path ?? ""), 42);
      const range = lineRange(args.offset as number | undefined, args.limit as number | undefined);
      return range ? `${path}:${range}` : path;
    }
    case "edit":
    case "write": return shortenPath(String(args.path ?? ""), 42);
    case "find":
    case "grep": return String(args.pattern ?? "");
    case "ls": return shortenPath(String(args.path ?? "."), 42);
    default: return undefined;
  }
}

const SERENA_COMPACT_TOOLS = new Set([
  "find_symbol",
  "find_referencing_symbols",
  "insert_after_symbol",
  "replace_symbol_body",
  "read_file",
  "get_symbols_overview",
  "insert_before_symbol",
  "rename_symbol",
  "restart_language_server",
  "jet_brains_get_symbols_overview",
  "jet_brains_find_symbol",
  "jet_brains_find_referencing_symbols",
  "jet_brains_type_hierarchy",
  "search_for_pattern",
  "list_dir",
  "find_file",
  "create_text_file",
  "replace_content",
  "delete_lines",
  "replace_lines",
  "insert_at_line",
  "execute_shell_command",
  "get_current_config",
  "activate_project",
  "remove_project",
  "switch_modes",
  "open_dashboard",
  "check_onboarding_performed",
  "onboarding",
  "initial_instructions",
  "prepare_for_new_conversation",
  "summarize_changes",
  "think_about_collected_information",
  "think_about_task_adherence",
  "think_about_whether_you_are_done",
  "read_memory",
  "write_memory",
  "list_memories",
  "delete_memory",
  "rename_memory",
  "edit_memory",
  "serena_mcp_reset",
]);

function parseJson(text: string): unknown | undefined {
  try {
    return JSON.parse(text);
  } catch {
    return undefined;
  }
}

function asRecord(value: unknown): Record<string, unknown> | undefined {
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
}

function countSearchMatches(payload: unknown): number | undefined {
  const record = asRecord(payload);
  if (!record) return undefined;
  let total = 0;
  for (const value of Object.values(record)) {
    if (Array.isArray(value)) total += value.length;
  }
  return total > 0 ? total : undefined;
}

function countOverviewSymbols(payload: unknown): number {
  if (Array.isArray(payload)) {
    const nested = payload.reduce<number>((total, value) => total + countOverviewSymbols(value), 0);
    return nested || payload.length;
  }
  const record = asRecord(payload);
  if (!record) return 0;
  return Object.values(record).reduce<number>((total, value) => total + countOverviewSymbols(value), 0);
}

function countLines(text: string): number {
  if (!text) return 0;
  return text.split("\n").length;
}

function countJsonItems(payload: unknown): number | undefined {
  if (Array.isArray(payload)) return payload.length;
  const record = asRecord(payload);
  if (!record) return undefined;

  let total = 0;
  for (const value of Object.values(record)) {
    if (Array.isArray(value)) total += value.length;
  }
  return total > 0 ? total : undefined;
}

function summarizeSerenaSubject(toolName: string, input: Record<string, unknown>): string | undefined {
  switch (toolName) {
    case "find_symbol":
    case "find_referencing_symbols":
    case "replace_symbol_body":
    case "insert_after_symbol":
    case "insert_before_symbol":
    case "rename_symbol":
    case "jet_brains_find_symbol":
    case "jet_brains_find_referencing_symbols":
    case "jet_brains_type_hierarchy":
      return String(input.name_path_pattern ?? input.name_path ?? "symbol");
    case "get_symbols_overview":
    case "jet_brains_get_symbols_overview":
    case "read_file":
    case "create_text_file":
    case "replace_content":
    case "replace_lines":
    case "delete_lines":
    case "insert_at_line":
    case "list_dir":
    case "find_file":
      return shortenPath(String(input.relative_path ?? input.path ?? "."), 42);
    case "search_for_pattern":
      return shortenCommand(String(input.substring_pattern ?? ""), 52);
    case "read_memory":
    case "write_memory":
    case "delete_memory":
    case "rename_memory":
    case "edit_memory":
      return String(input.memory_name ?? input.old_name ?? "memory");
    case "activate_project":
    case "remove_project":
      return String(input.project ?? input.project_name ?? "project");
    case "switch_modes": {
      const modes = input.modes;
      if (Array.isArray(modes)) return modes.map((mode) => String(mode)).join(",");
      return "modes";
    }
    case "execute_shell_command":
      return shortenCommand(String(input.command ?? ""), 52);
    default:
      return undefined;
  }
}

function summarizeSerenaToolResult(
  toolName: string,
  input: Record<string, unknown>,
  text: string,
  durationMs: number | undefined,
): string {
  const payload = parseJson(text);
  const duration = formatDuration(durationMs);
  const payloadSize = formatPayloadSize(text);
  const subject = summarizeSerenaSubject(toolName, input);
  const meta = (...parts: Array<string | undefined>) => {
    const joined = joinCompactMeta([duration, payloadSize, ...parts]);
    return joined ? ` · ${joined}` : "";
  };

  switch (toolName) {
    case "find_symbol":
    case "find_referencing_symbols":
    case "jet_brains_find_symbol":
    case "jet_brains_find_referencing_symbols": {
      const count = countJsonItems(payload) ?? (text.match(/"name_path"\s*:/g)?.length ?? 0);
      return `${TOOL_ROW_MARKER} serena ${toolName} ${subject ?? "symbol"}${meta(formatLineLabel(count, "result"))}`;
    }
    case "get_symbols_overview":
    case "jet_brains_get_symbols_overview":
    case "jet_brains_type_hierarchy": {
      const count = Math.max(countOverviewSymbols(payload), text.match(/"name_path"\s*:/g)?.length ?? 0);
      return `${TOOL_ROW_MARKER} serena ${toolName} ${subject ?? "file"}${meta(formatLineLabel(count, "symbol"))}`;
    }
    case "search_for_pattern": {
      const count = countSearchMatches(payload) ?? (text.match(/^\s*>\s*\d+:/gm)?.length ?? 0);
      return `${TOOL_ROW_MARKER} serena search ${subject ?? "pattern"}${meta(formatLineLabel(count, "match"))}`;
    }
    case "read_file": {
      return `${TOOL_ROW_MARKER} serena read ${subject ?? "file"}${meta(formatLineLabel(countLines(text), "line"))}`;
    }
    case "list_dir": {
      const count = countJsonItems(payload) ?? countLines(text);
      return `${TOOL_ROW_MARKER} serena list_dir ${subject ?? "."}${meta(formatLineLabel(count, "entry"))}`;
    }
    case "find_file": {
      const count = countJsonItems(payload) ?? countLines(text);
      return `${TOOL_ROW_MARKER} serena find_file ${String(input.file_mask ?? "")}${meta(formatLineLabel(count, "match"))}`;
    }
    case "replace_symbol_body":
    case "insert_after_symbol":
    case "insert_before_symbol":
    case "rename_symbol":
    case "create_text_file":
    case "replace_content":
    case "replace_lines":
    case "delete_lines":
    case "insert_at_line":
    case "write_memory":
    case "delete_memory":
    case "rename_memory":
    case "edit_memory":
    case "activate_project":
    case "remove_project":
    case "switch_modes":
    case "restart_language_server":
    case "onboarding":
    case "serena_mcp_reset":
      return `${TOOL_ROW_MARKER} serena ${toolName}${subject ? ` ${subject}` : ""}${meta()}`;
    case "execute_shell_command": {
      const count = countLines(text);
      return `${TOOL_ROW_MARKER} serena shell ${subject ?? "command"}${meta(formatLineLabel(count, "line"))}`;
    }
    default: {
      const count = countJsonItems(payload) ?? countLines(text);
      return `${TOOL_ROW_MARKER} serena ${toolName}${subject ? ` ${subject}` : ""}${meta(formatLineLabel(count, "item"))}`;
    }
  }
}


function formatHierarchyText(text: string): string {
  const lines = text.split("\n");
  const out: string[] = [];
  for (const raw of lines) {
    const line = raw.trimEnd();
    if (!line.trim()) {
      out.push("");
      continue;
    }
    if (line.startsWith("● ")) {
      out.push(line);
      continue;
    }
    if (/^(Read|Searched|Listed|Update\(|File must be read first|Now )/.test(line.trimStart())) {
      out.push(`  └─ ${line.trimStart()}`);
      continue;
    }
    out.push(line);
  }
  return out.join("\n");
}

function normalizeToolLabel(toolName: string): string {
  const gitnexusMap: Record<string, string> = {
    gitnexus_query: "gitnexus query",
    gitnexus_context: "gitnexus context",
    gitnexus_impact: "gitnexus impact",
    gitnexus_detect_changes: "gitnexus detect_changes",
    gitnexus_list_repos: "gitnexus list_repos",
    gitnexus_rename: "gitnexus rename",
    gitnexus_cypher: "gitnexus cypher",
  };

  if (gitnexusMap[toolName]) return gitnexusMap[toolName];

  if (toolName.startsWith("gitnexus_")) {
    return `gitnexus ${toolName.slice("gitnexus_".length)}`;
  }

  const idx = toolName.indexOf("_");
  if (idx > 0) {
    const head = toolName.slice(0, idx);
    const tail = toolName.slice(idx + 1);
    if (head && tail) return `${head} ${tail}`;
  }

  return toolName;
}

function summarizeGenericToolResult(
  toolName: string,
  input: Record<string, unknown>,
  text: string,
  durationMs: number | undefined,
): string {
  const payload = parseJson(text);
  const duration = formatDuration(durationMs);
  const payloadSize = formatPayloadSize(text);
  const subject = summarizeToolSubject(toolName, input) ?? summarizeSerenaSubject(toolName, input);
  const count = countJsonItems(payload) ?? countLines(text);
  const label = formatLineLabel(count, "line");
  const joined = joinCompactMeta([duration, payloadSize, label]);
  const normalized = normalizeToolLabel(toolName);
  return `${TOOL_ROW_MARKER} ${normalized}${subject ? ` ${subject}` : ""}${joined ? ` · ${joined}` : ""}`;
}

function summarizeStructuredReturnToolResult(
  input: Record<string, unknown>,
  text: string,
  details: unknown,
  durationMs: number | undefined,
): string {
  const record = asRecord(details);
  const command = shortenCommand(String(input.command ?? text.split("→")[0] ?? "command"), 52);
  const resultText = text.includes("→") ? text.split("→").slice(1).join("→").trim() : text.trim();
  const resultLines = resultText.split("\n").map((line) => line.trim()).filter(Boolean);
  const summary = resultLines.find((line) => !line.startsWith("cwd:"));
  const parser = typeof record?.parser === "string" ? record.parser : undefined;
  const exitCode = typeof record?.exitCode === "number" ? `exit ${record.exitCode}` : undefined;
  const resultMeta = joinCompactMeta([formatDuration(durationMs), formatPayloadSize(text)]);
  const meta = joinMeta([summary ? shortenCommand(summary, 72) : undefined, parser, exitCode, resultMeta]);
  return `${TOOL_ROW_MARKER} structured_return ${command}${meta ? ` · ${meta}` : ""}`;
}

function summarizeProcessToolResult(
  input: Record<string, unknown>,
  text: string,
  details: unknown,
  durationMs: number | undefined,
): string {
  const record = asRecord(details);
  const action = String(record?.action ?? input.action ?? "action");
  const duration = formatDuration(durationMs);
  const payloadSize = formatPayloadSize(text);
  const meta = (...parts: Array<string | undefined>) => {
    const joined = joinCompactMeta([duration, payloadSize, ...parts]);
    return joined ? ` · ${joined}` : "";
  };

  if (action === "start") {
    const proc = asRecord(record?.process);
    const name = String(proc?.name ?? input.name ?? "process");
    const id = proc?.id ? String(proc.id) : undefined;
    const pid = proc?.pid != null ? `pid ${String(proc.pid)}` : undefined;
    return `${TOOL_ROW_MARKER} process start "${name}"${meta(id, pid)}`;
  }

  if (action === "list") {
    const processes = Array.isArray(record?.processes) ? record.processes : [];
    const running = processes.filter((item) => {
      const proc = asRecord(item);
      return proc?.status === "running" || proc?.status === "terminating";
    }).length;
    return `${TOOL_ROW_MARKER} process list${meta(`${processes.length} ${processes.length === 1 ? "process" : "processes"}`, `${running} running`)}`;
  }

  if (action === "output") {
    const output = asRecord(record?.output);
    const stdout = Array.isArray(output?.stdout) ? output.stdout.length : undefined;
    const stderr = Array.isArray(output?.stderr) ? output.stderr.length : undefined;
    return `${TOOL_ROW_MARKER} process output ${String(input.id ?? "process")}${meta(
      stdout != null ? `${stdout} stdout` : undefined,
      stderr != null ? `${stderr} stderr` : undefined,
    )}`;
  }

  if (action === "logs") {
    return `${TOOL_ROW_MARKER} process logs ${String(input.id ?? "process")}${meta("log paths")}`;
  }

  const message = typeof record?.message === "string" ? record.message : text.split("\n")[0];
  return `${TOOL_ROW_MARKER} process ${action}${message ? ` · ${shortenCommand(message, 38)}` : ""}${meta()}`;
}

function summarizeExternalToolResult(
  toolName: string,
  input: Record<string, unknown>,
  text: string,
  details: unknown,
  durationMs: number | undefined,
): string {
  if (SERENA_COMPACT_TOOLS.has(toolName)) {
    return summarizeSerenaToolResult(toolName, input, text, durationMs);
  }
  if (toolName === "structured_return") {
    return summarizeStructuredReturnToolResult(input, text, details, durationMs);
  }
  if (toolName === "process") {
    return summarizeProcessToolResult(input, text, details, durationMs);
  }
  return summarizeGenericToolResult(toolName, input, text, durationMs);
}

function withXtrmToolDetails(details: unknown, sourceText: string, toolName: string): unknown {
  const record = asRecord(details);
  return {
    ...(record ?? {}),
    xtrmOriginalText: sourceText,
    xtrmToolFrame: externalToolFrameKind(toolName),
  };
}

const XTRM_BUILTIN_TOOLS = new Set(["bash", "read", "edit", "write", "find", "grep", "ls"]);

// Tools whose RESULT PAYLOAD the model needs verbatim in context. In pi, a tool_result
// hook's return value REPLACES the model-facing content (not just the TUI render), so
// compacting these would replace the model-facing payload with "· N lines" / "· N results"
// for every pi surface that loads xtrm-ui — interactive sessions (where the human can
// expand the TUI row but the model cannot) and any headless pi run that doesn't pass
// --no-extensions. Specialists are unaffected: they run with --no-extensions and never
// load xtrm-ui (selectively re-attach only quality-gates / service-skills / pi-gitnexus /
// pi-serena-tools). Mutation/no-payload tools are still summarized below, preserving
// most of the token savings.
const PAYLOAD_TOOLS = new Set<string>([
  // Serena reads / inspection
  "read_file",
  "find_symbol",
  "find_referencing_symbols",
  "get_symbols_overview",
  "search_for_pattern",
  "find_file",
  "list_dir",
  "read_memory",
  // JetBrains backend equivalents
  "jet_brains_find_symbol",
  "jet_brains_find_referencing_symbols",
  "jet_brains_get_symbols_overview",
  "jet_brains_type_hierarchy",
  // GitNexus reads (same blindness risk)
  "gitnexus_query",
  "gitnexus_context",
  "gitnexus_impact",
  "gitnexus_detect_changes",
]);

function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): void {
  const activeToolCalls = new Map<string, string>();

  const toolRowText = (theme: any, text: string) =>
    new Text(
      text,
      0,
      0,
      getPrefs().toolRowBg ? (line: string) => theme.bg("selectedBg", line) : undefined,
    );
  const activeSignatureCounts = new Map<string, number>();
  const toolCallStartTimes = new Map<string, number>();

  const trackToolCallStart = (toolCallId: string, toolName: string, args: Record<string, unknown>) => {
    const signature = stableToolSignature(toolName, args);
    activeToolCalls.set(toolCallId, signature);
    activeSignatureCounts.set(signature, (activeSignatureCounts.get(signature) ?? 0) + 1);
    toolCallStartTimes.set(toolCallId, Date.now());
  };

  const trackToolCallEnd = (toolCallId: string) => {
    const signature = activeToolCalls.get(toolCallId);
    if (!signature) return;
    activeToolCalls.delete(toolCallId);
    const next = (activeSignatureCounts.get(signature) ?? 1) - 1;
    if (next <= 0) activeSignatureCounts.delete(signature);
    else activeSignatureCounts.set(signature, next);
    toolCallStartTimes.delete(toolCallId);
  };

  const isToolCallActive = (toolName: string, args: Record<string, unknown>) =>
    activeSignatureCounts.has(stableToolSignature(toolName, args));

  const renderPendingCallIfActive = (toolName: string, args: Record<string, unknown>, theme: any) =>
    isToolCallActive(toolName, args) ? renderPendingCall(toolName, args, theme) : toolRowText(theme, "");

  pi.on("tool_call", async (event) => {
    trackToolCallStart(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
  });

  pi.on("tool_execution_end", async (event) => {
    trackToolCallEnd(event.toolCallId);
  });

  pi.on("tool_result", async (event: ToolResultEvent, _ctx) => {
    if (event.isError) return undefined;
    if (XTRM_BUILTIN_TOOLS.has(event.toolName)) {
      trackToolCallEnd(event.toolCallId);
      return undefined;
    }
    // Never compact read/inspect tools: the hook return replaces model-facing content,
    // so summarizing these would blind headless agents/specialists (no row to expand).
    if (PAYLOAD_TOOLS.has(event.toolName)) return undefined;
    if (!getPrefs().compactExternalToolResults) return undefined;

    const text = getTextContent({ content: event.content as Array<{ type: string; text?: string }> });
    const startedAt = toolCallStartTimes.get(event.toolCallId);
    const durationMs = startedAt != null ? Date.now() - startedAt : undefined;

    const fallbackText = "[non-text result]";
    const sourceText = text.trim() ? text : fallbackText;

    const safeInput =
      event.input && typeof event.input === "object" && !Array.isArray(event.input)
        ? (event.input as Record<string, unknown>)
        : {};

    const compactText = summarizeExternalToolResult(
      event.toolName,
      safeInput,
      sourceText,
      event.details,
      durationMs,
    );

    return {
      content: [{ type: "text", text: formatHierarchyText(compactText) }],
      details: withXtrmToolDetails(event.details, sourceText, event.toolName),
    };
  });

  pi.registerTool({
    name: "bash",
    label: "bash",
    description: getTools(process.cwd()).bash.description,
    parameters: getTools(process.cwd()).bash.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const result = await getTools(ctx.cwd).bash.execute(toolCallId, params, signal, onUpdate);
      return { ...result, details: withXtrmMeta(result.details as BashToolDetails | undefined, "bash", params as Record<string, unknown>, Date.now() - started) };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("bash", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<BashToolDetails, Record<string, unknown>>;
      const meta = getXtrmMeta<BashToolDetails, Record<string, unknown>>(details);
      const command = shortenCommand(String(meta?.args.command ?? ""), 80);
      if (isPartial) {
        return toolRowText(theme, `${theme.fg("accent", TOOL_ROW_MARKER)} ${theme.fg("toolTitle", "bash:")}${theme.fg("accent", command)}`);
      }
      const output = getTextContent(result as any);
      const outputLines = cleanOutputLines(output);
      const exitMatch = output.match(/exit code:\s*(-?\d+)/i);
      const exitCode = exitMatch ? Number.parseInt(exitMatch[1] ?? "0", 10) : 0;
      const bullet = exitCode === 0 ? theme.fg("success", TOOL_ROW_MARKER) : theme.fg("error", TOOL_ROW_MARKER);
      const summary = joinCompactMeta([
        formatDuration(meta?.durationMs),
        formatPayloadSize(output),
        formatLineLabel(outputLines.length, "line"),
        details.truncation?.truncated ? "truncated" : undefined,
      ]);
      let text = `${bullet} ${theme.fg("toolTitle", "bash:")}${theme.fg("accent", command)}`;
      if (summary) text += theme.fg("dim", ` · ${summary}`);
      if (expanded && outputLines.length > 0) text += `\n${renderVerticalPreview(theme, outputLines, 10)}`;
      return toolRowText(theme, text);
    },
  });

  pi.registerTool({
    name: "read",
    label: "read",
    description: getTools(process.cwd()).read.description,
    parameters: getTools(process.cwd()).read.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const result = await getTools(ctx.cwd).read.execute(toolCallId, params, signal, onUpdate);
      return { ...result, details: withXtrmMeta(result.details as ReadToolDetails | undefined, "read", params as Record<string, unknown>, Date.now() - started) };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("read", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "read", "loading", undefined));
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<ReadToolDetails, Record<string, unknown>>;
      const meta = getXtrmMeta<ReadToolDetails, Record<string, unknown>>(details);
      const subjectBase = shortenPath(String(meta?.args.path ?? ""));
      const range = lineRange(meta?.args.offset as number | undefined, meta?.args.limit as number | undefined);
      const subject = range ? `${subjectBase}:${range}` : subjectBase;
      const first = result.content[0];
      if (first?.type === "image") {
        return toolRowText(theme, renderToolSummary(theme, "success", "read", subject, joinMeta(["image", formatDuration(meta?.durationMs)])));
      }
      const textContent = getTextContent(result as any);
      const lines = textContent.split("\n");
      let text = renderToolSummary(theme, "success", "read", subject, joinMeta([formatLineLabel(lines.length, "line"), formatDuration(meta?.durationMs), details.truncation?.truncated ? `from ${details.truncation.totalLines}` : undefined]));
      if (expanded && textContent.length > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 14), 14)}`;
      return toolRowText(theme, text);
    },
  });

  pi.registerTool({
    name: "edit",
    label: "edit",
    description: getTools(process.cwd()).edit.description,
    parameters: getTools(process.cwd()).edit.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const result = await getTools(ctx.cwd).edit.execute(toolCallId, params, signal, onUpdate);
      return { ...result, details: withXtrmMeta(result.details as EditToolDetails | undefined, "edit", params as Record<string, unknown>, Date.now() - started) };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("edit", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "edit", "applying", undefined));
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<EditToolDetails, Record<string, unknown>>;
      const meta = getXtrmMeta<EditToolDetails, Record<string, unknown>>(details);
      const textContent = getTextContent(result as any);
      if (/^error/i.test(textContent.trim())) {
        return toolRowText(theme, renderToolSummary(theme, "error", "edit", shortenPath(String(meta?.args.path ?? "")), textContent.split("\n")[0]));
      }
      const stats = details.diff ? diffStats(details.diff) : { additions: 0, removals: 0 };
      let text = renderToolSummary(theme, "success", "edit", shortenPath(String(meta?.args.path ?? "")), joinMeta([`+${stats.additions}`, `-${stats.removals}`, formatDuration(meta?.durationMs)]));
      if (expanded && details.diff) text += `\n${renderRichDiffPreview(theme, details.diff, 18)}`;
      return toolRowText(theme, text);
    },
  });

  pi.registerTool({
    name: "write",
    label: "write",
    description: getTools(process.cwd()).write.description,
    parameters: getTools(process.cwd()).write.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const args = params as Record<string, unknown>;
      const path = String(args.path ?? "");
      const content = String(args.content ?? "");
      const preview = createWritePreview(path, content);
      const result = await getTools(ctx.cwd).write.execute(toolCallId, params, signal, onUpdate);
      const details = withXtrmMeta(result.details as Record<string, never> | undefined, "write", args, Date.now() - started);
      return { ...result, details: { ...details, xtrmWritePreview: preview } };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("write", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "write", "writing", undefined));
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<Record<string, never>, Record<string, unknown>>;
      const meta = getXtrmMeta<Record<string, never>, Record<string, unknown>>(details);
      const textContent = getTextContent(result as any);
      if (/^error/i.test(textContent.trim())) {
        return toolRowText(theme, renderToolSummary(theme, "error", "write", shortenPath(String(meta?.args.path ?? "")), textContent.split("\n")[0]));
      }

      const subject = shortenPath(String(meta?.args.path ?? ""));
      const preview = details.xtrmWritePreview;

      if (preview?.kind === "unchanged") {
        return toolRowText(theme, renderToolSummary(theme, "success", "write", subject, joinMeta(["no changes", formatDuration(meta?.durationMs)])));
      }

      if (preview?.kind === "updated") {
        let text = renderToolSummary(
          theme,
          "success",
          "write",
          subject,
          joinMeta([`+${preview.additions}`, `-${preview.removals}`, formatDuration(meta?.durationMs)]),
        );
        if (expanded && preview.diff) text += `\n${renderRichDiffPreview(theme, preview.diff, 18)}`;
        return toolRowText(theme, text);
      }

      const lines = preview?.kind === "created"
        ? preview.lineCount
        : lineCount(String(meta?.args.content ?? ""));

      return toolRowText(theme, renderToolSummary(
          theme,
          "success",
          "write",
          subject,
          joinMeta([formatLineLabel(lines, "line"), formatDuration(meta?.durationMs)]),
        ),
        0,
        0,
      );
    },
  });

  pi.registerTool({
    name: "find",
    label: "find",
    description: getTools(process.cwd()).find.description,
    parameters: getTools(process.cwd()).find.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const result = await getTools(ctx.cwd).find.execute(toolCallId, params, signal, onUpdate);
      return { ...result, details: withXtrmMeta(result.details as FindToolDetails | undefined, "find", params as Record<string, unknown>, Date.now() - started) };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("find", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "find", "searching", undefined));
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<FindToolDetails, Record<string, unknown>>;
      const meta = getXtrmMeta<FindToolDetails, Record<string, unknown>>(details);
      const textContent = getTextContent(result as any);
      const count = summarizeCount(textContent);
      let text = renderToolSummary(theme, "success", "find", String(meta?.args.pattern ?? ""), joinMeta([formatLineLabel(count, "match"), formatDuration(meta?.durationMs), details.resultLimitReached ? "limit reached" : undefined]));
      if (expanded && count > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 10), 10)}`;
      return toolRowText(theme, text);
    },
  });

  pi.registerTool({
    name: "grep",
    label: "grep",
    description: getTools(process.cwd()).grep.description,
    parameters: getTools(process.cwd()).grep.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const result = await getTools(ctx.cwd).grep.execute(toolCallId, params, signal, onUpdate);
      return { ...result, details: withXtrmMeta(result.details as GrepToolDetails | undefined, "grep", params as Record<string, unknown>, Date.now() - started) };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("grep", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "grep", "searching", undefined));
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<GrepToolDetails, Record<string, unknown>>;
      const meta = getXtrmMeta<GrepToolDetails, Record<string, unknown>>(details);
      const textContent = getTextContent(result as any);
      const count = countPrefixedItems(textContent, ["-- "]) || summarizeCount(textContent);
      let text = renderToolSummary(theme, "success", "grep", String(meta?.args.pattern ?? ""), joinMeta([formatLineLabel(count, "match"), formatDuration(meta?.durationMs), details.matchLimitReached ? "limit reached" : undefined]));
      if (expanded && textContent.length > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 12), 12)}`;
      return toolRowText(theme, text);
    },
  });

  pi.registerTool({
    name: "ls",
    label: "ls",
    description: getTools(process.cwd()).ls.description,
    parameters: getTools(process.cwd()).ls.parameters,
    renderShell: "self",
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      const started = Date.now();
      const result = await getTools(ctx.cwd).ls.execute(toolCallId, params, signal, onUpdate);
      return { ...result, details: withXtrmMeta(result.details as LsToolDetails | undefined, "ls", params as Record<string, unknown>, Date.now() - started) };
    },
    renderCall: (args, theme) => renderPendingCallIfActive("ls", args as Record<string, unknown>, theme),
    renderResult(result, { expanded, isPartial }, theme) {
      if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "ls", "listing", undefined));
      const details = (result.details ?? {}) as DetailsWithXtrmMeta<LsToolDetails, Record<string, unknown>>;
      const meta = getXtrmMeta<LsToolDetails, Record<string, unknown>>(details);
      const textContent = getTextContent(result as any);
      const count = summarizeCount(textContent);
      let text = renderToolSummary(theme, "success", "ls", shortenPath(String(meta?.args.path ?? ".")), joinMeta([formatLineLabel(count, "entry"), formatDuration(meta?.durationMs), details.entryLimitReached ? "limit reached" : undefined]));
      if (expanded && count > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 12), 12)}`;
      return toolRowText(theme, text);
    },
  });
}

// ============================================================================
// Main Extension
// ============================================================================

function isXtrmTheme(name: string | undefined): boolean {
  return name === "pidex-dark" || name === "pidex-light";
}

export default function xtrmUiExtension(pi: ExtensionAPI): void {
  void installSilentHiddenThinkingPatch().catch(() => undefined);
  void installExternalToolFramePatch().catch(() => undefined);

  let prefs: XtrmUiPrefs = { ...DEFAULT_PREFS };
  let previousThemeName: string | null = null;
  const extensionThemeDir = join(__dirname, "../../themes/xtrm-ui");

  const getPrefs = () => prefs;
  const setPrefs = (p: XtrmUiPrefs) => {
    prefs = p;
    setActiveExternalToolChrome(p.externalToolChrome);
  };
  const getThinkingLevel = () => formatThinking(pi.getThinkingLevel());

  registerXtrmUiTools(pi, getPrefs);
  registerCommands(pi, getPrefs, setPrefs, getThinkingLevel);

  const refresh = (ctx: ExtensionContext) => {
    applyXtrmChrome(ctx, prefs, getThinkingLevel);
    applyThinkingChrome(ctx, prefs);
  };

  pi.on("resources_discover", async () => ({
    themePaths: [extensionThemeDir],
  }));

  pi.on("session_start", async (_event, ctx) => {
    setPrefs(loadPrefs(ctx.sessionManager.getEntries() as Array<MaybeCustomEntry>));
    if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
      previousThemeName = ctx.ui.theme.name ?? null;
    }
    refresh(ctx);

    setTimeout(() => {
      ctx.ui.setTheme(resolveThemeForPrefs(prefs));
    }, 0);
  });

  pi.on("session_switch", async (_event, ctx) => {
    if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
      previousThemeName = ctx.ui.theme.name ?? null;
    }
    refresh(ctx);
  });

  pi.on("session_fork", async (_event, ctx) => {
    if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
      previousThemeName = ctx.ui.theme.name ?? null;
    }
    refresh(ctx);
  });

  pi.on("model_select", async (_event, ctx) => {
    refresh(ctx);
  });

  pi.on("session_shutdown", async (_event, ctx) => {
    if (previousThemeName) {
      ctx.ui.setTheme(previousThemeName);
    }
  });

  pi.on("input", async (event) => {
    if (event.source === "extension") return { action: "continue" as const };
    if (!event.text.trim()) return { action: "continue" as const };
    if (event.text.startsWith("/") || event.text.startsWith("!")) return { action: "continue" as const };
    if (event.text.startsWith("› ")) return { action: "continue" as const };
    return event.images
      ? { action: "transform" as const, text: `› ${event.text}`, images: event.images }
      : { action: "transform" as const, text: `› ${event.text}` };
  });

  pi.on("context", async (event) => {
    const messages = event.messages.map((message) => {
      if (message.role === "user" && typeof message.content === "string" && message.content.startsWith("› ")) {
        return { ...message, content: message.content.slice(2) };
      }
      if (message.role === "user" && Array.isArray(message.content)) {
        return {
          ...message,
          content: message.content.map((item, index) =>
            index === 0 && item.type === "text" && item.text.startsWith("› ")
              ? { ...item, text: item.text.slice(2) }
              : item
          ),
        };
      }
      return message;
    });
    return { messages };
  });
}
