import { Container, getKeybindings, Input, Spacer, Text, TruncatedText, truncateToWidth, } from "@earendil-works/pi-tui";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
import { keyHint, keyText } from "./keybinding-hints.js";
class TreeList {
    flatNodes = [];
    filteredNodes = [];
    selectedIndex = 0;
    currentLeafId;
    maxVisibleLines;
    filterMode = "default";
    searchQuery = "";
    toolCallMap = new Map();
    multipleRoots = false;
    showLabelTimestamps = false;
    activePathIds = new Set();
    visibleParentMap = new Map();
    visibleChildrenMap = new Map();
    lastSelectedId = null;
    foldedNodes = new Set();
    onSelect;
    onCancel;
    onLabelEdit;
    constructor(tree, currentLeafId, maxVisibleLines, initialSelectedId, initialFilterMode) {
        this.currentLeafId = currentLeafId;
        this.maxVisibleLines = maxVisibleLines;
        this.filterMode = initialFilterMode ?? "default";
        this.multipleRoots = tree.length > 1;
        this.flatNodes = this.flattenTree(tree);
        this.buildActivePath();
        this.applyFilter();
        // Start with initialSelectedId if provided, otherwise current leaf
        const targetId = initialSelectedId ?? currentLeafId;
        this.selectedIndex = this.findNearestVisibleIndex(targetId);
        this.lastSelectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id ?? null;
    }
    /**
     * Find the index of the nearest visible entry, walking up the parent chain if needed.
     * Returns the index in filteredNodes, or the last index as fallback.
     */
    findNearestVisibleIndex(entryId) {
        if (this.filteredNodes.length === 0)
            return 0;
        // Build a map for parent lookup
        const entryMap = new Map();
        for (const flatNode of this.flatNodes) {
            entryMap.set(flatNode.node.entry.id, flatNode);
        }
        // Build a map of visible entry IDs to their indices in filteredNodes
        const visibleIdToIndex = new Map(this.filteredNodes.map((node, i) => [node.node.entry.id, i]));
        // Walk from entryId up to root, looking for a visible entry
        let currentId = entryId;
        while (currentId !== null) {
            const index = visibleIdToIndex.get(currentId);
            if (index !== undefined)
                return index;
            const node = entryMap.get(currentId);
            if (!node)
                break;
            currentId = node.node.entry.parentId ?? null;
        }
        // Fallback: last visible entry
        return this.filteredNodes.length - 1;
    }
    /** Build the set of entry IDs on the path from root to current leaf */
    buildActivePath() {
        this.activePathIds.clear();
        if (!this.currentLeafId)
            return;
        // Build a map of id -> entry for parent lookup
        const entryMap = new Map();
        for (const flatNode of this.flatNodes) {
            entryMap.set(flatNode.node.entry.id, flatNode);
        }
        // Walk from leaf to root
        let currentId = this.currentLeafId;
        while (currentId) {
            this.activePathIds.add(currentId);
            const node = entryMap.get(currentId);
            if (!node)
                break;
            currentId = node.node.entry.parentId ?? null;
        }
    }
    flattenTree(roots) {
        const result = [];
        this.toolCallMap.clear();
        const stack = [];
        // Determine which subtrees contain the active leaf (to sort current branch first)
        // Use iterative post-order traversal to avoid stack overflow
        const containsActive = new Map();
        const leafId = this.currentLeafId;
        {
            // Build list in pre-order, then process in reverse for post-order effect
            const allNodes = [];
            const preOrderStack = [...roots];
            while (preOrderStack.length > 0) {
                const node = preOrderStack.pop();
                allNodes.push(node);
                // Push children in reverse so they're processed left-to-right
                for (let i = node.children.length - 1; i >= 0; i--) {
                    preOrderStack.push(node.children[i]);
                }
            }
            // Process in reverse (post-order): children before parents
            for (let i = allNodes.length - 1; i >= 0; i--) {
                const node = allNodes[i];
                let has = leafId !== null && node.entry.id === leafId;
                for (const child of node.children) {
                    if (containsActive.get(child)) {
                        has = true;
                    }
                }
                containsActive.set(node, has);
            }
        }
        // Add roots in reverse order, prioritizing the one containing the active leaf
        // If multiple roots, treat them as children of a virtual root that branches
        const multipleRoots = roots.length > 1;
        const orderedRoots = [...roots].sort((a, b) => Number(containsActive.get(b)) - Number(containsActive.get(a)));
        for (let i = orderedRoots.length - 1; i >= 0; i--) {
            const isLast = i === orderedRoots.length - 1;
            stack.push([orderedRoots[i], multipleRoots ? 1 : 0, multipleRoots, multipleRoots, isLast, [], multipleRoots]);
        }
        while (stack.length > 0) {
            const [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
            // Extract tool calls from assistant messages for later lookup
            const entry = node.entry;
            if (entry.type === "message" && entry.message.role === "assistant") {
                const content = entry.message.content;
                if (Array.isArray(content)) {
                    for (const block of content) {
                        if (typeof block === "object" && block !== null && "type" in block && block.type === "toolCall") {
                            const tc = block;
                            this.toolCallMap.set(tc.id, { name: tc.name, arguments: tc.arguments });
                        }
                    }
                }
            }
            result.push({ node, indent, showConnector, isLast, gutters, isVirtualRootChild });
            const children = node.children;
            const multipleChildren = children.length > 1;
            // Order children so the branch containing the active leaf comes first
            const orderedChildren = (() => {
                const prioritized = [];
                const rest = [];
                for (const child of children) {
                    if (containsActive.get(child)) {
                        prioritized.push(child);
                    }
                    else {
                        rest.push(child);
                    }
                }
                return [...prioritized, ...rest];
            })();
            // Calculate child indent
            let childIndent;
            if (multipleChildren) {
                // Parent branches: children get +1
                childIndent = indent + 1;
            }
            else if (justBranched && indent > 0) {
                // First generation after a branch: +1 for visual grouping
                childIndent = indent + 1;
            }
            else {
                // Single-child chain: stay flat
                childIndent = indent;
            }
            // Build gutters for children
            // If this node showed a connector, add a gutter entry for descendants
            // Only add gutter if connector is actually displayed (not suppressed for virtual root children)
            const connectorDisplayed = showConnector && !isVirtualRootChild;
            // When connector is displayed, add a gutter entry at the connector's position
            // Connector is at position (displayIndent - 1), so gutter should be there too
            const currentDisplayIndent = this.multipleRoots ? Math.max(0, indent - 1) : indent;
            const connectorPosition = Math.max(0, currentDisplayIndent - 1);
            const childGutters = connectorDisplayed
                ? [...gutters, { position: connectorPosition, show: !isLast }]
                : gutters;
            // Add children in reverse order
            for (let i = orderedChildren.length - 1; i >= 0; i--) {
                const childIsLast = i === orderedChildren.length - 1;
                stack.push([
                    orderedChildren[i],
                    childIndent,
                    multipleChildren,
                    multipleChildren,
                    childIsLast,
                    childGutters,
                    false,
                ]);
            }
        }
        return result;
    }
    applyFilter() {
        // Update lastSelectedId only when we have a valid selection (non-empty list)
        // This preserves the selection when switching through empty filter results
        if (this.filteredNodes.length > 0) {
            this.lastSelectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id ?? this.lastSelectedId;
        }
        const searchTokens = this.searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
        this.filteredNodes = this.flatNodes.filter((flatNode) => {
            const entry = flatNode.node.entry;
            const isCurrentLeaf = entry.id === this.currentLeafId;
            // Skip assistant messages with only tool calls (no text) unless error/aborted
            // Always show current leaf so active position is visible
            if (entry.type === "message" && entry.message.role === "assistant" && !isCurrentLeaf) {
                const msg = entry.message;
                const hasText = this.hasTextContent(msg.content);
                const isErrorOrAborted = msg.stopReason && msg.stopReason !== "stop" && msg.stopReason !== "toolUse";
                // Only hide if no text AND not an error/aborted message
                if (!hasText && !isErrorOrAborted) {
                    return false;
                }
            }
            // Apply filter mode
            let passesFilter = true;
            // Entry types hidden in default view (settings/bookkeeping)
            const isSettingsEntry = entry.type === "label" ||
                entry.type === "custom" ||
                entry.type === "model_change" ||
                entry.type === "thinking_level_change" ||
                entry.type === "session_info";
            switch (this.filterMode) {
                case "user-only":
                    // Just user messages
                    passesFilter = entry.type === "message" && entry.message.role === "user";
                    break;
                case "no-tools":
                    // Default minus tool results
                    passesFilter = !isSettingsEntry && !(entry.type === "message" && entry.message.role === "toolResult");
                    break;
                case "labeled-only":
                    // Just labeled entries
                    passesFilter = flatNode.node.label !== undefined;
                    break;
                case "all":
                    // Show everything
                    passesFilter = true;
                    break;
                default:
                    // Default mode: hide settings/bookkeeping entries
                    passesFilter = !isSettingsEntry;
                    break;
            }
            if (!passesFilter)
                return false;
            // Apply search filter
            if (searchTokens.length > 0) {
                const nodeText = this.getSearchableText(flatNode.node).toLowerCase();
                return searchTokens.every((token) => nodeText.includes(token));
            }
            return true;
        });
        // Filter out descendants of folded nodes.
        if (this.foldedNodes.size > 0) {
            const skipSet = new Set();
            for (const flatNode of this.flatNodes) {
                const { id, parentId } = flatNode.node.entry;
                if (parentId != null && (this.foldedNodes.has(parentId) || skipSet.has(parentId))) {
                    skipSet.add(id);
                }
            }
            this.filteredNodes = this.filteredNodes.filter((flatNode) => !skipSet.has(flatNode.node.entry.id));
        }
        // Recalculate visual structure (indent, connectors, gutters) based on visible tree
        this.recalculateVisualStructure();
        // Try to preserve cursor on the same node, or find nearest visible ancestor
        if (this.lastSelectedId) {
            this.selectedIndex = this.findNearestVisibleIndex(this.lastSelectedId);
        }
        else if (this.selectedIndex >= this.filteredNodes.length) {
            // Clamp index if out of bounds
            this.selectedIndex = Math.max(0, this.filteredNodes.length - 1);
        }
        // Update lastSelectedId to the actual selection (may have changed due to parent walk)
        if (this.filteredNodes.length > 0) {
            this.lastSelectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id ?? this.lastSelectedId;
        }
    }
    /**
     * Recompute indentation/connectors for the filtered view
     *
     * Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor.
     * Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right.
     */
    recalculateVisualStructure() {
        if (this.filteredNodes.length === 0)
            return;
        const visibleIds = new Set(this.filteredNodes.map((n) => n.node.entry.id));
        // Build entry map for efficient parent lookup (using full tree)
        const entryMap = new Map();
        for (const flatNode of this.flatNodes) {
            entryMap.set(flatNode.node.entry.id, flatNode);
        }
        // Find nearest visible ancestor for a node
        const findVisibleAncestor = (nodeId) => {
            let currentId = entryMap.get(nodeId)?.node.entry.parentId ?? null;
            while (currentId !== null) {
                if (visibleIds.has(currentId)) {
                    return currentId;
                }
                currentId = entryMap.get(currentId)?.node.entry.parentId ?? null;
            }
            return null;
        };
        // Build visible tree structure:
        // - visibleParent: nodeId → nearest visible ancestor (or null for roots)
        // - visibleChildren: parentId → list of visible children (in filteredNodes order)
        const visibleParent = new Map();
        const visibleChildren = new Map();
        visibleChildren.set(null, []); // root-level nodes
        for (const flatNode of this.filteredNodes) {
            const nodeId = flatNode.node.entry.id;
            const ancestorId = findVisibleAncestor(nodeId);
            visibleParent.set(nodeId, ancestorId);
            if (!visibleChildren.has(ancestorId)) {
                visibleChildren.set(ancestorId, []);
            }
            visibleChildren.get(ancestorId).push(nodeId);
        }
        // Update multipleRoots based on visible roots
        const visibleRootIds = visibleChildren.get(null);
        this.multipleRoots = visibleRootIds.length > 1;
        // Build a map for quick lookup: nodeId → FlatNode
        const filteredNodeMap = new Map();
        for (const flatNode of this.filteredNodes) {
            filteredNodeMap.set(flatNode.node.entry.id, flatNode);
        }
        const stack = [];
        // Add visible roots in reverse order (to process in forward order via stack)
        for (let i = visibleRootIds.length - 1; i >= 0; i--) {
            const isLast = i === visibleRootIds.length - 1;
            stack.push([
                visibleRootIds[i],
                this.multipleRoots ? 1 : 0,
                this.multipleRoots,
                this.multipleRoots,
                isLast,
                [],
                this.multipleRoots,
            ]);
        }
        while (stack.length > 0) {
            const [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
            const flatNode = filteredNodeMap.get(nodeId);
            if (!flatNode)
                continue;
            // Update this node's visual properties
            flatNode.indent = indent;
            flatNode.showConnector = showConnector;
            flatNode.isLast = isLast;
            flatNode.gutters = gutters;
            flatNode.isVirtualRootChild = isVirtualRootChild;
            // Get visible children of this node
            const children = visibleChildren.get(nodeId) || [];
            const multipleChildren = children.length > 1;
            // Child indent follows flattenTree(): branch points (and first generation after a branch) shift +1
            let childIndent;
            if (multipleChildren) {
                childIndent = indent + 1;
            }
            else if (justBranched && indent > 0) {
                childIndent = indent + 1;
            }
            else {
                childIndent = indent;
            }
            // Child gutters follow flattenTree() connector/gutter rules
            const connectorDisplayed = showConnector && !isVirtualRootChild;
            const currentDisplayIndent = this.multipleRoots ? Math.max(0, indent - 1) : indent;
            const connectorPosition = Math.max(0, currentDisplayIndent - 1);
            const childGutters = connectorDisplayed
                ? [...gutters, { position: connectorPosition, show: !isLast }]
                : gutters;
            // Add children in reverse order (to process in forward order via stack)
            for (let i = children.length - 1; i >= 0; i--) {
                const childIsLast = i === children.length - 1;
                stack.push([
                    children[i],
                    childIndent,
                    multipleChildren,
                    multipleChildren,
                    childIsLast,
                    childGutters,
                    false,
                ]);
            }
        }
        // Store visible tree maps for ancestor/descendant lookups in navigation
        this.visibleParentMap = visibleParent;
        this.visibleChildrenMap = visibleChildren;
    }
    /** Get searchable text content from a node */
    getSearchableText(node) {
        const entry = node.entry;
        const parts = [];
        if (node.label) {
            parts.push(node.label);
        }
        switch (entry.type) {
            case "message": {
                const msg = entry.message;
                parts.push(msg.role);
                if ("content" in msg && msg.content) {
                    parts.push(this.extractContent(msg.content));
                }
                if (msg.role === "bashExecution") {
                    const bashMsg = msg;
                    if (bashMsg.command)
                        parts.push(bashMsg.command);
                }
                break;
            }
            case "custom_message": {
                parts.push(entry.customType);
                if (typeof entry.content === "string") {
                    parts.push(entry.content);
                }
                else {
                    parts.push(this.extractContent(entry.content));
                }
                break;
            }
            case "compaction":
                parts.push("compaction");
                break;
            case "branch_summary":
                parts.push("branch summary", entry.summary);
                break;
            case "session_info":
                parts.push("title");
                if (entry.name)
                    parts.push(entry.name);
                break;
            case "model_change":
                parts.push("model", entry.modelId);
                break;
            case "thinking_level_change":
                parts.push("thinking", entry.thinkingLevel);
                break;
            case "custom":
                parts.push("custom", entry.customType);
                break;
            case "label":
                parts.push("label", entry.label ?? "");
                break;
        }
        return parts.join(" ");
    }
    invalidate() { }
    getSearchQuery() {
        return this.searchQuery;
    }
    getSelectedNode() {
        return this.filteredNodes[this.selectedIndex]?.node;
    }
    updateNodeLabel(entryId, label, labelTimestamp) {
        for (const flatNode of this.flatNodes) {
            if (flatNode.node.entry.id === entryId) {
                flatNode.node.label = label;
                flatNode.node.labelTimestamp = label ? (labelTimestamp ?? new Date().toISOString()) : undefined;
                break;
            }
        }
    }
    getStatusLabels() {
        let labels = "";
        switch (this.filterMode) {
            case "no-tools":
                labels += " [no-tools]";
                break;
            case "user-only":
                labels += " [user]";
                break;
            case "labeled-only":
                labels += " [labeled]";
                break;
            case "all":
                labels += " [all]";
                break;
        }
        if (this.showLabelTimestamps) {
            labels += " [+label time]";
        }
        return labels;
    }
    render(width) {
        const lines = [];
        if (this.filteredNodes.length === 0) {
            lines.push(truncateToWidth(theme.fg("muted", "  No entries found"), width));
            lines.push(truncateToWidth(theme.fg("muted", `  (0/0)${this.getStatusLabels()}`), width));
            return lines;
        }
        const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisibleLines / 2), this.filteredNodes.length - this.maxVisibleLines));
        const endIndex = Math.min(startIndex + this.maxVisibleLines, this.filteredNodes.length);
        for (let i = startIndex; i < endIndex; i++) {
            const flatNode = this.filteredNodes[i];
            const entry = flatNode.node.entry;
            const isSelected = i === this.selectedIndex;
            // Build line: cursor + prefix + path marker + label + content
            const cursor = isSelected ? theme.fg("accent", "› ") : "  ";
            // If multiple roots, shift display (roots at 0, not 1)
            const displayIndent = this.multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent;
            // Build prefix with gutters at their correct positions
            // Each gutter has a position (displayIndent where its connector was shown)
            const connector = flatNode.showConnector && !flatNode.isVirtualRootChild ? (flatNode.isLast ? "└─ " : "├─ ") : "";
            const connectorPosition = connector ? displayIndent - 1 : -1;
            // Build prefix char by char, placing gutters and connector at their positions
            const totalChars = displayIndent * 3;
            const prefixChars = [];
            const isFolded = this.foldedNodes.has(entry.id);
            for (let i = 0; i < totalChars; i++) {
                const level = Math.floor(i / 3);
                const posInLevel = i % 3;
                // Check if there's a gutter at this level
                const gutter = flatNode.gutters.find((g) => g.position === level);
                if (gutter) {
                    if (posInLevel === 0) {
                        prefixChars.push(gutter.show ? "│" : " ");
                    }
                    else {
                        prefixChars.push(" ");
                    }
                }
                else if (connector && level === connectorPosition) {
                    // Connector at this level, with fold indicator
                    if (posInLevel === 0) {
                        prefixChars.push(flatNode.isLast ? "└" : "├");
                    }
                    else if (posInLevel === 1) {
                        const foldable = this.isFoldable(entry.id);
                        prefixChars.push(isFolded ? "⊞" : foldable ? "⊟" : "─");
                    }
                    else {
                        prefixChars.push(" ");
                    }
                }
                else {
                    prefixChars.push(" ");
                }
            }
            const prefix = prefixChars.join("");
            // Fold marker for nodes without connectors (roots)
            const showsFoldInConnector = flatNode.showConnector && !flatNode.isVirtualRootChild;
            const foldMarker = isFolded && !showsFoldInConnector ? theme.fg("accent", "⊞ ") : "";
            // Active path marker - shown right before the entry text
            const isOnActivePath = this.activePathIds.has(entry.id);
            const pathMarker = isOnActivePath ? theme.fg("accent", "• ") : "";
            const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
            const labelTimestamp = this.showLabelTimestamps && flatNode.node.label && flatNode.node.labelTimestamp
                ? theme.fg("muted", `${this.formatLabelTimestamp(flatNode.node.labelTimestamp)} `)
                : "";
            const content = this.getEntryDisplayText(flatNode.node, isSelected);
            let line = cursor + theme.fg("dim", prefix) + foldMarker + pathMarker + label + labelTimestamp + content;
            if (isSelected) {
                line = theme.bg("selectedBg", line);
            }
            lines.push(truncateToWidth(line, width));
        }
        lines.push(truncateToWidth(theme.fg("muted", `  (${this.selectedIndex + 1}/${this.filteredNodes.length})${this.getStatusLabels()}`), width));
        return lines;
    }
    getEntryDisplayText(node, isSelected) {
        const entry = node.entry;
        let result;
        const normalize = (s) => s.replace(/[\n\t]/g, " ").trim();
        switch (entry.type) {
            case "message": {
                const msg = entry.message;
                const role = msg.role;
                if (role === "user") {
                    const msgWithContent = msg;
                    const content = normalize(this.extractContent(msgWithContent.content));
                    result = theme.fg("accent", "user: ") + content;
                }
                else if (role === "assistant") {
                    const msgWithContent = msg;
                    const textContent = normalize(this.extractContent(msgWithContent.content));
                    if (textContent) {
                        result = theme.fg("success", "assistant: ") + textContent;
                    }
                    else if (msgWithContent.stopReason === "aborted") {
                        result = theme.fg("success", "assistant: ") + theme.fg("muted", "(aborted)");
                    }
                    else if (msgWithContent.errorMessage) {
                        const errMsg = normalize(msgWithContent.errorMessage).slice(0, 80);
                        result = theme.fg("success", "assistant: ") + theme.fg("error", errMsg);
                    }
                    else {
                        result = theme.fg("success", "assistant: ") + theme.fg("muted", "(no content)");
                    }
                }
                else if (role === "toolResult") {
                    const toolMsg = msg;
                    const toolCall = toolMsg.toolCallId ? this.toolCallMap.get(toolMsg.toolCallId) : undefined;
                    if (toolCall) {
                        result = theme.fg("muted", this.formatToolCall(toolCall.name, toolCall.arguments));
                    }
                    else {
                        result = theme.fg("muted", `[${toolMsg.toolName ?? "tool"}]`);
                    }
                }
                else if (role === "bashExecution") {
                    const bashMsg = msg;
                    result = theme.fg("dim", `[bash]: ${normalize(bashMsg.command ?? "")}`);
                }
                else {
                    result = theme.fg("dim", `[${role}]`);
                }
                break;
            }
            case "custom_message": {
                const content = typeof entry.content === "string"
                    ? entry.content
                    : entry.content
                        .filter((c) => c.type === "text")
                        .map((c) => c.text)
                        .join("");
                result = theme.fg("customMessageLabel", `[${entry.customType}]: `) + normalize(content);
                break;
            }
            case "compaction": {
                const tokens = Math.round(entry.tokensBefore / 1000);
                result = theme.fg("borderAccent", `[compaction: ${tokens}k tokens]`);
                break;
            }
            case "branch_summary":
                result = theme.fg("warning", `[branch summary]: `) + normalize(entry.summary);
                break;
            case "model_change":
                result = theme.fg("dim", `[model: ${entry.modelId}]`);
                break;
            case "thinking_level_change":
                result = theme.fg("dim", `[thinking: ${entry.thinkingLevel}]`);
                break;
            case "custom":
                result = theme.fg("dim", `[custom: ${entry.customType}]`);
                break;
            case "label":
                result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`);
                break;
            case "session_info":
                result = entry.name
                    ? [theme.fg("dim", "[title: "), theme.fg("dim", entry.name), theme.fg("dim", "]")].join("")
                    : [theme.fg("dim", "[title: "), theme.italic(theme.fg("dim", "empty")), theme.fg("dim", "]")].join("");
                break;
            default:
                result = "";
        }
        return isSelected ? theme.bold(result) : result;
    }
    formatLabelTimestamp(timestamp) {
        const date = new Date(timestamp);
        const now = new Date();
        const hours = date.getHours().toString().padStart(2, "0");
        const minutes = date.getMinutes().toString().padStart(2, "0");
        const time = `${hours}:${minutes}`;
        if (date.getFullYear() === now.getFullYear() &&
            date.getMonth() === now.getMonth() &&
            date.getDate() === now.getDate()) {
            return time;
        }
        const month = date.getMonth() + 1;
        const day = date.getDate();
        if (date.getFullYear() === now.getFullYear()) {
            return `${month}/${day} ${time}`;
        }
        const year = date.getFullYear().toString().slice(-2);
        return `${year}/${month}/${day} ${time}`;
    }
    extractContent(content) {
        const maxLen = 200;
        if (typeof content === "string")
            return content.slice(0, maxLen);
        if (Array.isArray(content)) {
            let result = "";
            for (const c of content) {
                if (typeof c === "object" && c !== null && "type" in c && c.type === "text") {
                    result += c.text;
                    if (result.length >= maxLen)
                        return result.slice(0, maxLen);
                }
            }
            return result;
        }
        return "";
    }
    hasTextContent(content) {
        if (typeof content === "string")
            return content.trim().length > 0;
        if (Array.isArray(content)) {
            for (const c of content) {
                if (typeof c === "object" && c !== null && "type" in c && c.type === "text") {
                    const text = c.text;
                    if (text && text.trim().length > 0)
                        return true;
                }
            }
        }
        return false;
    }
    formatToolCall(name, args) {
        const shortenPath = (p) => {
            const home = process.env.HOME || process.env.USERPROFILE || "";
            if (home && p.startsWith(home))
                return `~${p.slice(home.length)}`;
            return p;
        };
        switch (name) {
            case "read": {
                const path = shortenPath(String(args.path || args.file_path || ""));
                const offset = args.offset;
                const limit = args.limit;
                let display = path;
                if (offset !== undefined || limit !== undefined) {
                    const start = offset ?? 1;
                    const end = limit !== undefined ? start + limit - 1 : "";
                    display += `:${start}${end ? `-${end}` : ""}`;
                }
                return `[read: ${display}]`;
            }
            case "write": {
                const path = shortenPath(String(args.path || args.file_path || ""));
                return `[write: ${path}]`;
            }
            case "edit": {
                const path = shortenPath(String(args.path || args.file_path || ""));
                return `[edit: ${path}]`;
            }
            case "bash": {
                const rawCmd = String(args.command || "");
                const cmd = rawCmd
                    .replace(/[\n\t]/g, " ")
                    .trim()
                    .slice(0, 50);
                return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`;
            }
            case "grep": {
                const pattern = String(args.pattern || "");
                const path = shortenPath(String(args.path || "."));
                return `[grep: /${pattern}/ in ${path}]`;
            }
            case "find": {
                const pattern = String(args.pattern || "");
                const path = shortenPath(String(args.path || "."));
                return `[find: ${pattern} in ${path}]`;
            }
            case "ls": {
                const path = shortenPath(String(args.path || "."));
                return `[ls: ${path}]`;
            }
            default: {
                // Custom tool - show name and truncated JSON args
                const argsStr = JSON.stringify(args).slice(0, 40);
                return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? "..." : ""}]`;
            }
        }
    }
    handleInput(keyData) {
        const kb = getKeybindings();
        if (kb.matches(keyData, "tui.select.up")) {
            this.selectedIndex = this.selectedIndex === 0 ? this.filteredNodes.length - 1 : this.selectedIndex - 1;
        }
        else if (kb.matches(keyData, "tui.select.down")) {
            this.selectedIndex = this.selectedIndex === this.filteredNodes.length - 1 ? 0 : this.selectedIndex + 1;
        }
        else if (kb.matches(keyData, "app.tree.foldOrUp")) {
            const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
            if (currentId && this.isFoldable(currentId) && !this.foldedNodes.has(currentId)) {
                this.foldedNodes.add(currentId);
                this.applyFilter();
            }
            else {
                this.selectedIndex = this.findBranchSegmentStart("up");
            }
        }
        else if (kb.matches(keyData, "app.tree.unfoldOrDown")) {
            const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
            if (currentId && this.foldedNodes.has(currentId)) {
                this.foldedNodes.delete(currentId);
                this.applyFilter();
            }
            else {
                this.selectedIndex = this.findBranchSegmentStart("down");
            }
        }
        else if (kb.matches(keyData, "tui.editor.cursorLeft") || kb.matches(keyData, "tui.select.pageUp")) {
            // Page up
            this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisibleLines);
        }
        else if (kb.matches(keyData, "tui.editor.cursorRight") || kb.matches(keyData, "tui.select.pageDown")) {
            // Page down
            this.selectedIndex = Math.min(this.filteredNodes.length - 1, this.selectedIndex + this.maxVisibleLines);
        }
        else if (kb.matches(keyData, "tui.select.confirm")) {
            const selected = this.filteredNodes[this.selectedIndex];
            if (selected && this.onSelect) {
                this.onSelect(selected.node.entry.id);
            }
        }
        else if (kb.matches(keyData, "tui.select.cancel")) {
            if (this.searchQuery) {
                this.searchQuery = "";
                this.foldedNodes.clear();
                this.applyFilter();
            }
            else {
                this.onCancel?.();
            }
        }
        else if (kb.matches(keyData, "app.tree.filter.default")) {
            // Direct filter: default
            this.filterMode = "default";
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "app.tree.filter.noTools")) {
            // Toggle filter: no-tools ↔ default
            this.filterMode = this.filterMode === "no-tools" ? "default" : "no-tools";
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "app.tree.filter.userOnly")) {
            // Toggle filter: user-only ↔ default
            this.filterMode = this.filterMode === "user-only" ? "default" : "user-only";
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "app.tree.filter.labeledOnly")) {
            // Toggle filter: labeled-only ↔ default
            this.filterMode = this.filterMode === "labeled-only" ? "default" : "labeled-only";
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "app.tree.filter.all")) {
            // Toggle filter: all ↔ default
            this.filterMode = this.filterMode === "all" ? "default" : "all";
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "app.tree.filter.cycleBackward")) {
            // Cycle filter backwards
            const modes = ["default", "no-tools", "user-only", "labeled-only", "all"];
            const currentIndex = modes.indexOf(this.filterMode);
            this.filterMode = modes[(currentIndex - 1 + modes.length) % modes.length];
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "app.tree.filter.cycleForward")) {
            // Cycle filter forwards: default → no-tools → user-only → labeled-only → all → default
            const modes = ["default", "no-tools", "user-only", "labeled-only", "all"];
            const currentIndex = modes.indexOf(this.filterMode);
            this.filterMode = modes[(currentIndex + 1) % modes.length];
            this.foldedNodes.clear();
            this.applyFilter();
        }
        else if (kb.matches(keyData, "tui.editor.deleteCharBackward")) {
            if (this.searchQuery.length > 0) {
                this.searchQuery = this.searchQuery.slice(0, -1);
                this.foldedNodes.clear();
                this.applyFilter();
            }
        }
        else if (kb.matches(keyData, "app.tree.editLabel")) {
            const selected = this.filteredNodes[this.selectedIndex];
            if (selected && this.onLabelEdit) {
                this.onLabelEdit(selected.node.entry.id, selected.node.label);
            }
        }
        else if (kb.matches(keyData, "app.tree.toggleLabelTimestamp")) {
            this.showLabelTimestamps = !this.showLabelTimestamps;
        }
        else {
            const hasControlChars = [...keyData].some((ch) => {
                const code = ch.charCodeAt(0);
                return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
            });
            if (!hasControlChars && keyData.length > 0) {
                this.searchQuery += keyData;
                this.foldedNodes.clear();
                this.applyFilter();
            }
        }
    }
    /**
     * Whether a node can be folded. A node is foldable if it has visible children
     * and is either a root (no visible parent) or a segment start (visible parent
     * has multiple visible children).
     */
    isFoldable(entryId) {
        const children = this.visibleChildrenMap.get(entryId);
        if (!children || children.length === 0)
            return false;
        const parentId = this.visibleParentMap.get(entryId);
        if (parentId === null || parentId === undefined)
            return true;
        const siblings = this.visibleChildrenMap.get(parentId);
        return siblings !== undefined && siblings.length > 1;
    }
    /**
     * Find the index of the next branch segment start in the given direction.
     * A segment start is the first child of a branch point.
     *
     * "up" walks the visible parent chain; "down" walks visible children
     * (always following the first child).
     */
    findBranchSegmentStart(direction) {
        const selectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
        if (!selectedId)
            return this.selectedIndex;
        const indexByEntryId = new Map(this.filteredNodes.map((node, i) => [node.node.entry.id, i]));
        let currentId = selectedId;
        if (direction === "down") {
            while (true) {
                const children = this.visibleChildrenMap.get(currentId) ?? [];
                if (children.length === 0)
                    return indexByEntryId.get(currentId);
                if (children.length > 1)
                    return indexByEntryId.get(children[0]);
                currentId = children[0];
            }
        }
        // direction === "up"
        while (true) {
            const parentId = this.visibleParentMap.get(currentId) ?? null;
            if (parentId === null)
                return indexByEntryId.get(currentId);
            const children = this.visibleChildrenMap.get(parentId) ?? [];
            if (children.length > 1) {
                const segmentStart = indexByEntryId.get(currentId);
                if (segmentStart < this.selectedIndex) {
                    return segmentStart;
                }
            }
            currentId = parentId;
        }
    }
}
/** Component that displays the current search query */
class SearchLine {
    treeList;
    constructor(treeList) {
        this.treeList = treeList;
    }
    invalidate() { }
    render(width) {
        const query = this.treeList.getSearchQuery();
        if (query) {
            return [truncateToWidth(`  ${theme.fg("muted", "Type to search:")} ${theme.fg("accent", query)}`, width)];
        }
        return [truncateToWidth(`  ${theme.fg("muted", "Type to search:")}`, width)];
    }
    handleInput(_keyData) { }
}
/** Label input component shown when editing a label */
class LabelInput {
    input;
    entryId;
    onSubmit;
    onCancel;
    // Focusable implementation - propagate to input for IME cursor positioning
    _focused = false;
    get focused() {
        return this._focused;
    }
    set focused(value) {
        this._focused = value;
        this.input.focused = value;
    }
    constructor(entryId, currentLabel) {
        this.entryId = entryId;
        this.input = new Input();
        if (currentLabel) {
            this.input.setValue(currentLabel);
        }
    }
    invalidate() { }
    render(width) {
        const lines = [];
        const indent = "  ";
        const availableWidth = width - indent.length;
        lines.push(truncateToWidth(`${indent}${theme.fg("muted", "Label (empty to remove):")}`, width));
        lines.push(...this.input.render(availableWidth).map((line) => truncateToWidth(`${indent}${line}`, width)));
        lines.push(truncateToWidth(`${indent}${keyHint("tui.select.confirm", "save")}  ${keyHint("tui.select.cancel", "cancel")}`, width));
        return lines;
    }
    handleInput(keyData) {
        const kb = getKeybindings();
        if (kb.matches(keyData, "tui.select.confirm")) {
            const value = this.input.getValue().trim();
            this.onSubmit?.(this.entryId, value || undefined);
        }
        else if (kb.matches(keyData, "tui.select.cancel")) {
            this.onCancel?.();
        }
        else {
            this.input.handleInput(keyData);
        }
    }
}
/**
 * Component that renders a session tree selector for navigation
 */
export class TreeSelectorComponent extends Container {
    treeList;
    labelInput = null;
    labelInputContainer;
    treeContainer;
    onLabelChangeCallback;
    // Focusable implementation - propagate to labelInput when active for IME cursor positioning
    _focused = false;
    get focused() {
        return this._focused;
    }
    set focused(value) {
        this._focused = value;
        // Propagate to labelInput when it's active
        if (this.labelInput) {
            this.labelInput.focused = value;
        }
    }
    constructor(tree, currentLeafId, terminalHeight, onSelect, onCancel, onLabelChange, initialSelectedId, initialFilterMode) {
        super();
        this.onLabelChangeCallback = onLabelChange;
        const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2));
        this.treeList = new TreeList(tree, currentLeafId, maxVisibleLines, initialSelectedId, initialFilterMode);
        this.treeList.onSelect = onSelect;
        this.treeList.onCancel = onCancel;
        this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel);
        this.treeContainer = new Container();
        this.treeContainer.addChild(this.treeList);
        this.labelInputContainer = new Container();
        this.addChild(new Spacer(1));
        this.addChild(new DynamicBorder());
        this.addChild(new Text(theme.bold("  Session Tree"), 1, 0));
        const filterKeys = [
            keyText("app.tree.filter.default"),
            keyText("app.tree.filter.noTools"),
            keyText("app.tree.filter.userOnly"),
            keyText("app.tree.filter.labeledOnly"),
            keyText("app.tree.filter.all"),
        ].join("/");
        const cycleKeys = `${keyText("app.tree.filter.cycleForward")}/${keyText("app.tree.filter.cycleBackward")}`;
        this.addChild(new TruncatedText(theme.fg("muted", `  ↑/↓: move. ←/→: page. ^←/^→ or Alt+←/Alt+→: fold/branch. ${keyText("app.tree.editLabel")}: label. ${filterKeys}: filters (${cycleKeys} cycle). ${keyText("app.tree.toggleLabelTimestamp")}: label time`), 0, 0));
        this.addChild(new SearchLine(this.treeList));
        this.addChild(new DynamicBorder());
        this.addChild(new Spacer(1));
        this.addChild(this.treeContainer);
        this.addChild(this.labelInputContainer);
        this.addChild(new Spacer(1));
        this.addChild(new DynamicBorder());
        if (tree.length === 0) {
            setTimeout(() => onCancel(), 100);
        }
    }
    showLabelInput(entryId, currentLabel) {
        this.labelInput = new LabelInput(entryId, currentLabel);
        this.labelInput.onSubmit = (id, label) => {
            this.treeList.updateNodeLabel(id, label);
            this.onLabelChangeCallback?.(id, label);
            this.hideLabelInput();
        };
        this.labelInput.onCancel = () => this.hideLabelInput();
        // Propagate current focused state to the new labelInput
        this.labelInput.focused = this._focused;
        this.treeContainer.clear();
        this.labelInputContainer.clear();
        this.labelInputContainer.addChild(this.labelInput);
    }
    hideLabelInput() {
        this.labelInput = null;
        this.labelInputContainer.clear();
        this.treeContainer.clear();
        this.treeContainer.addChild(this.treeList);
    }
    handleInput(keyData) {
        if (this.labelInput) {
            this.labelInput.handleInput(keyData);
        }
        else {
            this.treeList.handleInput(keyData);
        }
    }
    getTreeList() {
        return this.treeList;
    }
}
//# sourceMappingURL=tree-selector.js.map