{"version":3,"file":"memory-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/memory-editor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACN,SAAS,EACT,KAAK,SAAS,EAKd,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAsBjF,MAAM,WAAW,qBAAqB;IACrC,cAAc,IAAI,SAAS,UAAU,EAAE,CAAC;IACxC,gBAAgB,IAAI,SAAS,qBAAqB,EAAE,CAAC;IACrD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAAC;IACxD,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5C,WAAW,IAAI,UAAU,EAAE,CAAC;IAC5B,aAAa,IAAI,IAAI,CAAC;CACtB;AAED,qBAAa,qBAAsB,SAAQ,SAAU,YAAW,SAAS;IACxE,OAAO,CAAC,IAAI,CAAsB;IAClC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAA0B;IAG/C,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,cAAc,CAAuB;IAE7C,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,aAAa,CAAa;IAElC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAM;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAM;IAEtC,kFAAkF;IAClF,OAAO,CAAC,aAAa,CAAuB;IAE5C,wFAAwF;IACxF,OAAO,CAAC,kBAAkB,CAAuB;IAEjD;;;;OAIG;IACH,OAAO,CAAC,WAAW,CAAqC;IAGxD,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAOzB;IAED,YAAY,SAAS,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,IAAI,EAOtE;IAMD,4DAA4D;IAC5D,OAAO,IAAI,IAAI,CAiCd;IAMD,UAAU,IAAI,IAAI,CAAG;IAErB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAuB9B;IAED,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,cAAc;IA+JtB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,gBAAgB;IA4DxB,OAAO,CAAC,oBAAoB;IAoB5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,iBAAiB;IAqCzB,OAAO,CAAC,yBAAyB;IA8BjC,OAAO,CAAC,uBAAuB;IAwD/B,OAAO,CAAC,wBAAwB;IA4BhC,OAAO,CAAC,8BAA8B;IA8BtC,OAAO,CAAC,qBAAqB;IA+G7B,OAAO,CAAC,4BAA4B;IAUpC,OAAO,CAAC,iBAAiB;IAwBzB,OAAO,CAAC,eAAe;IAqCvB,OAAO,CAAC,iBAAiB;IA6BzB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA0DjC;IAED,OAAO,CAAC,eAAe;IA6EvB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,kBAAkB;IA4J1B,OAAO,CAAC,wBAAwB;IAwKhC,OAAO,CAAC,sBAAsB;IA2C9B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,wBAAwB;IAgBhC,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,gBAAgB;IAYxB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;;OAGG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,aAAa;CAIrB","sourcesContent":["/**\n * Dedicated interactive memory editor UI component.\n * Displays session memory items with navigation, editing, and review capabilities.\n */\n\nimport {\n\tContainer,\n\ttype Focusable,\n\tgetEditorKeybindings,\n\tInput,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@apholdings/jensen-tui\";\nimport type { MemoryHistorySnapshot, MemoryItem } from \"../../../core/memory.js\";\nimport { computeMemorySnapshotDiff } from \"../../../core/memory-diff.js\";\nimport { MEMORY_STALE_AFTER_DAYS, type MemoryReviewItem, reviewMemoryItems } from \"../../../core/memory-review.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.js\";\n\ntype EditorMode =\n\t| \"list\"\n\t| \"add_key\"\n\t| \"add_value\"\n\t| \"edit_key\"\n\t| \"edit_value\"\n\t| \"confirm_delete\"\n\t| \"confirm_clear\"\n\t| \"review\"\n\t| \"history\"\n\t| \"history_detail\"\n\t| \"history_diff\";\n\n/** Section of the review view */\ntype ReviewSection = \"stale\" | \"fresh\";\n\nexport interface MemoryEditorCallbacks {\n\tgetMemoryItems(): readonly MemoryItem[];\n\tgetMemoryHistory(): readonly MemoryHistorySnapshot[];\n\tsetMemoryItem(key: string, value: string): MemoryItem[];\n\tdeleteMemoryItem(key: string): MemoryItem[];\n\tclearMemory(): MemoryItem[];\n\trequestRender(): void;\n}\n\nexport class MemoryEditorComponent extends Container implements Focusable {\n\tprivate mode: EditorMode = \"list\";\n\tprivate selectedIndex = 0;\n\tprivate reviewSection: ReviewSection = \"stale\";\n\n\t// Input state\n\tprivate keyInput = new Input();\n\tprivate valueInput = new Input();\n\tprivate editingItemKey: string | null = null;\n\n\tprivate callbacks: MemoryEditorCallbacks;\n\tprivate requestRender: () => void;\n\n\tprivate readonly maxVisible = 12;\n\tprivate readonly maxValuePreview = 40;\n\n\t/** Index of the base snapshot for diff mode (the earlier snapshot in the pair) */\n\tprivate diffBaseIndex: number | null = null;\n\n\t/** Index (in original chronological order) of the explicitly armed baseline snapshot */\n\tprivate armedBaselineIndex: number | null = null;\n\n\t/**\n\t * Tracks the explicit two-step compare chooser flow.\n\t * - \"none\": no compare chooser active\n\t * - \"target_picking\": baseline is armed; operator is selecting a target snapshot\n\t */\n\tprivate compareStep: \"none\" | \"target_picking\" = \"none\";\n\n\t// Focusable implementation\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tif (this.mode === \"add_key\" || this.mode === \"edit_key\") {\n\t\t\tthis.keyInput.focused = value;\n\t\t} else if (this.mode === \"add_value\" || this.mode === \"edit_value\") {\n\t\t\tthis.valueInput.focused = value;\n\t\t}\n\t}\n\n\tconstructor(callbacks: MemoryEditorCallbacks, requestRender: () => void) {\n\t\tsuper();\n\t\tthis.callbacks = callbacks;\n\t\tthis.requestRender = requestRender;\n\n\t\tthis.keyInput.onSubmit = () => this.onKeyInputSubmit();\n\t\tthis.valueInput.onSubmit = () => this.onValueInputSubmit();\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Public API\n\t// -------------------------------------------------------------------------\n\n\t/** Refresh the displayed list from current backend state */\n\trefresh(): void {\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tif (this.selectedIndex >= items.length) {\n\t\t\tthis.selectedIndex = Math.max(0, items.length - 1);\n\t\t}\n\t\t// In review mode, switch section if current one is empty\n\t\tif (this.mode === \"review\") {\n\t\t\tthis.syncReviewSection();\n\t\t}\n\t\t// In history mode, sync selection if snapshots changed\n\t\tif (this.mode === \"history\" || this.mode === \"history_detail\" || this.mode === \"history_diff\") {\n\t\t\tthis.syncHistorySelection();\n\t\t}\n\t\t// In history_diff, clamp diffBaseIndex to valid range\n\t\tif (this.mode === \"history_diff\" && this.diffBaseIndex !== null) {\n\t\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t\tthis.diffBaseIndex = Math.min(this.diffBaseIndex, Math.max(0, snapshots.length - 1));\n\t\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, snapshots.length - 1));\n\t\t}\n\t\t// Clamp armed baseline to valid range\n\t\tif (this.armedBaselineIndex !== null) {\n\t\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t\tif (this.armedBaselineIndex >= snapshots.length) {\n\t\t\t\tthis.armedBaselineIndex = snapshots.length > 0 ? snapshots.length - 1 : null;\n\t\t\t}\n\t\t}\n\t\t// When in compare chooser, exit cleanly if no snapshots\n\t\tif (this.compareStep === \"target_picking\") {\n\t\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t\tif (snapshots.length === 0) {\n\t\t\t\tthis.compareStep = \"none\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Rendering\n\t// -------------------------------------------------------------------------\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (\n\t\t\tthis.mode === \"list\" ||\n\t\t\tthis.mode === \"review\" ||\n\t\t\tthis.mode === \"history\" ||\n\t\t\tthis.mode === \"history_detail\" ||\n\t\t\tthis.mode === \"history_diff\" ||\n\t\t\tthis.mode === \"confirm_delete\" ||\n\t\t\tthis.mode === \"confirm_clear\"\n\t\t) {\n\t\t\treturn this.renderListMode(width);\n\t\t} else if (\n\t\t\tthis.mode === \"add_key\" ||\n\t\t\tthis.mode === \"edit_key\" ||\n\t\t\tthis.mode === \"add_value\" ||\n\t\t\tthis.mode === \"edit_value\"\n\t\t) {\n\t\t\treturn this.renderInputMode(width);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate renderListMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Title bar\n\t\tlines.push(...this.renderTitleBar(width));\n\n\t\tif (this.mode === \"review\") {\n\t\t\tlines.push(...this.renderReviewMode(width));\n\t\t} else if (this.mode === \"history\") {\n\t\t\tlines.push(...this.renderHistoryMode(width));\n\t\t} else if (this.mode === \"history_detail\") {\n\t\t\tlines.push(...this.renderHistoryDetailMode(width));\n\t\t} else if (this.mode === \"history_diff\") {\n\t\t\tlines.push(...this.renderHistoryDiffMode(width));\n\t\t} else if (this.mode === \"confirm_delete\" || this.mode === \"confirm_clear\") {\n\t\t\tlines.push(...this.renderConfirmMode(width));\n\t\t} else {\n\t\t\tlines.push(...this.renderItemList(width));\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate renderTitleBar(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst titleText =\n\t\t\tthis.mode === \"review\"\n\t\t\t\t? \"Session Memory — Review\"\n\t\t\t\t: this.mode === \"history\"\n\t\t\t\t\t? \"Memory History — Snapshots\"\n\t\t\t\t\t: this.mode === \"history_detail\"\n\t\t\t\t\t\t? \"Memory History — Snapshot Detail\"\n\t\t\t\t\t\t: this.mode === \"history_diff\"\n\t\t\t\t\t\t\t? \"Memory History — Snapshot Diff\"\n\t\t\t\t\t\t\t: this.mode === \"confirm_delete\"\n\t\t\t\t\t\t\t\t? \"Session Memory — Confirm Delete\"\n\t\t\t\t\t\t\t\t: this.mode === \"confirm_clear\"\n\t\t\t\t\t\t\t\t\t? \"Session Memory — Confirm Clear\"\n\t\t\t\t\t\t\t\t\t: \"Session Memory\";\n\n\t\tconst left = theme.bold(theme.fg(\"accent\", titleText));\n\t\tconst freshnessNote =\n\t\t\tthis.mode === \"history\" || this.mode === \"history_detail\" || this.mode === \"history_diff\"\n\t\t\t\t? theme.fg(\"muted\", \"  (snapshot timeline · not event log)\")\n\t\t\t\t: theme.fg(\"muted\", `  (freshness is heuristic · ${MEMORY_STALE_AFTER_DAYS}d threshold)`);\n\t\tconst right = freshnessNote;\n\t\tconst rightWidth = visibleWidth(right);\n\t\tconst availableLeft = Math.max(0, width - rightWidth - 1);\n\t\tconst leftTruncated = truncateToWidth(left, availableLeft, \"…\");\n\t\tconst spacing = Math.max(1, width - visibleWidth(leftTruncated) - rightWidth);\n\t\tlines.push(`${leftTruncated}${\" \".repeat(spacing)}${right}`);\n\n\t\t// Subtitle\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tconst reviewed = reviewMemoryItems([...items]);\n\t\tconst staleCount = reviewed.filter((r) => r.reviewRecommended).length;\n\t\tconst subtitleParts: string[] = [];\n\n\t\tif (this.mode === \"history\") {\n\t\t\t// History shows snapshot count, newest first\n\t\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t\tif (snapshots.length === 0) {\n\t\t\t\tsubtitleParts.push(theme.fg(\"dim\", \"No snapshots\"));\n\t\t\t} else {\n\t\t\t\tconst currentCount = snapshots.filter((s) => s.isCurrent).length;\n\t\t\t\tif (this.compareStep === \"target_picking\") {\n\t\t\t\t\t// Show baseline → target annotation during explicit chooser\n\t\t\t\t\t// In history mode, selectedIndex is reversed display order.\n\t\t\t\t\t// Convert to chronological for snapshot access.\n\t\t\t\t\tconst targetChronologicalIndex = snapshots.length - 1 - this.selectedIndex;\n\t\t\t\t\tconst baseline = snapshots[this.armedBaselineIndex!]!;\n\t\t\t\t\tconst targetSnapshot = snapshots[targetChronologicalIndex]!;\n\t\t\t\t\tconst baselineDate = new Date(baseline.recordedAt);\n\t\t\t\t\tconst targetDate = new Date(targetSnapshot.recordedAt);\n\t\t\t\t\tconst baselineLabel = this.getRelativeAgeLabel(baselineDate);\n\t\t\t\t\tconst targetLabel = this.getRelativeAgeLabel(targetDate);\n\t\t\t\t\tsubtitleParts.push(\n\t\t\t\t\t\ttheme.fg(\"warning\", `[baseline: ${baselineLabel}]`),\n\t\t\t\t\t\t\" \",\n\t\t\t\t\t\ttheme.fg(\"accent\", `[selecting target: ${targetLabel}]`),\n\t\t\t\t\t);\n\t\t\t\t\tif (this.armedBaselineIndex === targetChronologicalIndex) {\n\t\t\t\t\t\tsubtitleParts.push(\" \", theme.fg(\"dim\", \"(same snapshot)\"));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsubtitleParts.push(`${snapshots.length} snapshot${snapshots.length === 1 ? \"\" : \"s\"}`);\n\t\t\t\t\tif (currentCount > 0) {\n\t\t\t\t\t\tsubtitleParts.push(theme.fg(\"accent\", \"[current]\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (this.armedBaselineIndex !== null && this.armedBaselineIndex < snapshots.length) {\n\t\t\t\t\t\tconst baseline = snapshots[this.armedBaselineIndex]!;\n\t\t\t\t\t\tconst baselineDate = new Date(baseline.recordedAt);\n\t\t\t\t\t\tconst baselineLabel = this.getRelativeAgeLabel(baselineDate);\n\t\t\t\t\t\tconst baselineDisplayIndex = snapshots.length - this.armedBaselineIndex;\n\t\t\t\t\t\tsubtitleParts.push(\n\t\t\t\t\t\t\ttheme.fg(\"warning\", `[baseline: ${baselineLabel} · snap ${baselineDisplayIndex}]`),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.mode === \"history_detail\") {\n\t\t\t// History detail shows selected snapshot info\n\t\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t\tif (snapshots.length > 0) {\n\t\t\t\tconst selected = snapshots[this.selectedIndex];\n\t\t\t\tif (selected) {\n\t\t\t\t\tconst date = new Date(selected.recordedAt);\n\t\t\t\t\tif (this.compareStep === \"target_picking\") {\n\t\t\t\t\t\t// Show baseline → target annotation during explicit chooser\n\t\t\t\t\t\tconst baseline = snapshots[this.armedBaselineIndex!]!;\n\t\t\t\t\t\tconst baselineDate = new Date(baseline.recordedAt);\n\t\t\t\t\t\tconst targetDate = new Date(selected.recordedAt);\n\t\t\t\t\t\tconst baselineLabel = this.getRelativeAgeLabel(baselineDate);\n\t\t\t\t\t\tconst targetLabel = this.getRelativeAgeLabel(targetDate);\n\t\t\t\t\t\tsubtitleParts.push(\n\t\t\t\t\t\t\ttheme.fg(\"warning\", `[baseline: ${baselineLabel}]`),\n\t\t\t\t\t\t\t\" \",\n\t\t\t\t\t\t\ttheme.fg(\"accent\", `[selecting target: ${targetLabel}]`),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (this.armedBaselineIndex === this.selectedIndex) {\n\t\t\t\t\t\t\tsubtitleParts.push(\" \", theme.fg(\"dim\", \"(same snapshot)\"));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsubtitleParts.push(\n\t\t\t\t\t\t\ttheme.fg(\"dim\", `Snapshot ${this.selectedIndex + 1}/${snapshots.length}`),\n\t\t\t\t\t\t\t` · ${date.toLocaleString()}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (selected.isCurrent) {\n\t\t\t\t\t\t\tsubtitleParts.push(` ${theme.fg(\"accent\", \"[current]\")}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.mode === \"history_diff\") {\n\t\t\t// History diff shows the compared snapshots\n\t\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t\tif (snapshots.length > 0 && this.diffBaseIndex !== null) {\n\t\t\t\tconst base = snapshots[this.diffBaseIndex];\n\t\t\t\tconst target = snapshots[this.selectedIndex];\n\t\t\t\tif (base && target) {\n\t\t\t\t\tconst baseDate = new Date(base.recordedAt);\n\t\t\t\t\tconst targetDate = new Date(target.recordedAt);\n\t\t\t\t\tconst baseLabel = this.getRelativeAgeLabel(baseDate);\n\t\t\t\t\tconst targetLabel = this.getRelativeAgeLabel(targetDate);\n\t\t\t\t\tsubtitleParts.push(theme.fg(\"dim\", `${baseLabel} → ${targetLabel}`));\n\t\t\t\t\tif (this.diffBaseIndex === snapshots.length - 1) {\n\t\t\t\t\t\tsubtitleParts.push(theme.fg(\"accent\", \" [current→prev]\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsubtitleParts.push(\n\t\t\t\t\t\t\ttheme.fg(\"dim\", ` (snapshots ${this.diffBaseIndex + 1} → ${this.selectedIndex + 1})`),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.mode !== \"review\") {\n\t\t\tif (items.length === 0) {\n\t\t\t\tsubtitleParts.push(theme.fg(\"muted\", \"No items\"));\n\t\t\t} else {\n\t\t\t\tsubtitleParts.push(`${items.length} item${items.length === 1 ? \"\" : \"s\"}`);\n\t\t\t\tif (staleCount > 0) {\n\t\t\t\t\tsubtitleParts.push(theme.fg(\"warning\", `${staleCount} need${staleCount === 1 ? \"s\" : \"\"} review`));\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Review section tabs\n\t\t\tconst staleItems = reviewed.filter((r) => r.reviewRecommended);\n\t\t\tconst freshItems = reviewed.filter((r) => !r.reviewRecommended);\n\t\t\tconst tabStale = this.reviewSection === \"stale\" ? theme.fg(\"accent\", \"◉ Stale\") : theme.fg(\"dim\", \"○ Stale\");\n\t\t\tconst tabFresh = this.reviewSection === \"fresh\" ? theme.fg(\"accent\", \"◉ Fresh\") : theme.fg(\"dim\", \"○ Fresh\");\n\t\t\tconst countStale = theme.fg(\"warning\", `(${staleItems.length})`);\n\t\t\tconst countFresh = theme.fg(\"success\", `(${freshItems.length})`);\n\t\t\tsubtitleParts.push(`${tabStale} ${countStale}  ${tabFresh} ${countFresh}`);\n\t\t}\n\n\t\tif (subtitleParts.length > 0) {\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(subtitleParts.join(\"\"), width, \"…\")));\n\t\t}\n\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\treturn lines;\n\t}\n\n\tprivate renderItemList(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tconst reviewed = reviewMemoryItems([...items]);\n\n\t\tif (items.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\"  (no memory items — use a to add)\", width, \"…\")));\n\t\t} else {\n\t\t\t// Calculate visible range with selection-centered scrolling\n\t\t\tconst startIndex = Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), items.length - this.maxVisible),\n\t\t\t);\n\t\t\tconst endIndex = Math.min(startIndex + this.maxVisible, items.length);\n\n\t\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\t\tconst entry = reviewed[i]!;\n\t\t\t\tlines.push(this.renderItemLine(entry, i === this.selectedIndex, width));\n\t\t\t}\n\n\t\t\tif (startIndex > 0 || endIndex < items.length) {\n\t\t\t\tlines.push(theme.fg(\"muted\", `  (${this.selectedIndex + 1}/${items.length})`));\n\t\t\t}\n\t\t}\n\n\t\t// Action hints\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\tlines.push(...this.renderActionHints(width));\n\n\t\treturn lines;\n\t}\n\n\tprivate renderItemLine(entry: MemoryReviewItem, isSelected: boolean, width: number): string {\n\t\tconst { item, label, reviewRecommended } = entry;\n\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \"  \";\n\n\t\t// Status indicator\n\t\tconst status = reviewRecommended ? theme.fg(\"warning\", \"⚠\") : theme.fg(\"success\", \"✓\");\n\n\t\t// Age label\n\t\tconst ageText = theme.fg(\"dim\", `· ${label}`);\n\n\t\t// Stale marker\n\t\tconst staleMarker = reviewRecommended ? ` ${theme.fg(\"warning\", \"review\")}` : \"\";\n\n\t\t// Value preview\n\t\tconst valuePreview =\n\t\t\titem.value.length > this.maxValuePreview ? `${item.value.slice(0, this.maxValuePreview)}…` : item.value;\n\n\t\t// Assemble: cursor + status + key + value + age + stale\n\t\tconst keyText = theme.bold(theme.fg(\"text\", item.key));\n\t\tconst valueText = theme.fg(\"muted\", valuePreview);\n\t\tconst parts = [`${cursor}${status}`, keyText, \":\", valueText, ageText, staleMarker];\n\t\tconst rawLine = parts.join(\" \");\n\t\tconst truncated = truncateToWidth(rawLine, width, \"…\");\n\t\treturn isSelected ? theme.bg(\"selectedBg\", truncated) : truncated;\n\t}\n\n\tprivate renderReviewMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tconst reviewed = reviewMemoryItems([...items]);\n\n\t\tconst targetEntries =\n\t\t\tthis.reviewSection === \"stale\"\n\t\t\t\t? reviewed.filter((r) => r.reviewRecommended)\n\t\t\t\t: reviewed.filter((r) => !r.reviewRecommended);\n\n\t\tif (targetEntries.length === 0) {\n\t\t\tconst msg =\n\t\t\t\tthis.reviewSection === \"stale\"\n\t\t\t\t\t? theme.fg(\"success\", \"  No stale items\")\n\t\t\t\t\t: theme.fg(\"muted\", \"  No fresh items\");\n\t\t\tlines.push(truncateToWidth(msg, width, \"…\"));\n\t\t} else {\n\t\t\tconst startIndex = Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), targetEntries.length - this.maxVisible),\n\t\t\t);\n\t\t\tconst endIndex = Math.min(startIndex + this.maxVisible, targetEntries.length);\n\n\t\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\t\tconst entry = targetEntries[i]!;\n\t\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\t\tlines.push(this.renderReviewItemLine(entry, isSelected, width));\n\t\t\t}\n\n\t\t\tif (startIndex > 0 || endIndex < targetEntries.length) {\n\t\t\t\tlines.push(theme.fg(\"muted\", `  (${this.selectedIndex + 1}/${targetEntries.length})`));\n\t\t\t}\n\t\t}\n\n\t\t// Freshness disclaimer\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\tlines.push(\n\t\t\ttheme.fg(\n\t\t\t\t\"muted\",\n\t\t\t\ttruncateToWidth(\n\t\t\t\t\t`Freshness is heuristic. Items older than ${MEMORY_STALE_AFTER_DAYS} days may be outdated.`,\n\t\t\t\t\twidth,\n\t\t\t\t\t\"…\",\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t// Navigation hints for review\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\tlines.push(\n\t\t\ttruncateToWidth(\n\t\t\t\t[rawKeyHint(\"←→\", \"switch section\"), \"  \", keyHint(\"selectCancel\", \"back to list\")].join(\"\"),\n\t\t\t\twidth,\n\t\t\t\t\"…\",\n\t\t\t),\n\t\t);\n\n\t\treturn lines;\n\t}\n\n\tprivate renderReviewItemLine(entry: MemoryReviewItem, isSelected: boolean, width: number): string {\n\t\tconst { item, label, note } = entry;\n\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \"  \";\n\n\t\tconst ageText = theme.fg(\"dim\", `(${label})`);\n\t\tconst keyText = theme.bold(theme.fg(\"text\", item.key));\n\t\tconst valueText = theme.fg(\"muted\", truncateToWidth(item.value, this.maxValuePreview * 2, \"…\"));\n\t\tconst parts = [`${cursor}${keyText}`, \":\", valueText, ageText];\n\t\tconst rawLine = parts.join(\" \");\n\t\tconst truncated = truncateToWidth(rawLine, width, \"…\");\n\t\tlet line = isSelected ? theme.bg(\"selectedBg\", truncated) : truncated;\n\n\t\t// Add note on selected stale items\n\t\tif (isSelected && note) {\n\t\t\tline += `\\n${theme.fg(\"warning\", truncateToWidth(`  ${note}`, width, \"…\"))}`;\n\t\t}\n\n\t\treturn line;\n\t}\n\n\t/**\n\t * Format a date as a relative age label.\n\t */\n\tprivate getRelativeAgeLabel(date: Date): string {\n\t\tconst now = Date.now();\n\t\tconst diffMs = now - date.getTime();\n\t\tconst diffSecs = Math.floor(diffMs / 1000);\n\t\tconst diffMins = Math.floor(diffSecs / 60);\n\t\tconst diffHours = Math.floor(diffMins / 60);\n\t\tconst diffDays = Math.floor(diffHours / 24);\n\n\t\tif (diffSecs < 60) return \"just now\";\n\t\tif (diffMins < 60) return `${diffMins}m ago`;\n\t\tif (diffHours < 24) return `${diffHours}h ago`;\n\t\tif (diffDays < 30) return `${diffDays}d ago`;\n\t\tconst diffMonths = Math.floor(diffDays / 30);\n\t\tif (diffMonths < 12) return `${diffMonths}mo ago`;\n\t\tconst diffYears = Math.floor(diffMonths / 12);\n\t\treturn `${diffYears}y ago`;\n\t}\n\n\tprivate renderHistoryMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\n\t\tif (snapshots.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\"  No memory snapshots in current branch.\", width, \"…\")));\n\t\t} else {\n\t\t\t// Show newest first (current state most relevant)\n\t\t\t// But we need to calculate indices for selection\n\t\t\tconst reversedSnapshots = [...snapshots].reverse();\n\t\t\tconst maxVisible = this.maxVisible;\n\t\t\tconst startIndex = Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), reversedSnapshots.length - maxVisible),\n\t\t\t);\n\t\t\tconst endIndex = Math.min(startIndex + maxVisible, reversedSnapshots.length);\n\n\t\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\t\tconst snapshot = reversedSnapshots[i]!;\n\t\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\t\tconst chronologicalIndex = snapshots.length - 1 - i;\n\t\t\t\tlines.push(this.renderHistorySnapshotLine(snapshot, isSelected, width, chronologicalIndex));\n\t\t\t}\n\n\t\t\tif (startIndex > 0 || endIndex < reversedSnapshots.length) {\n\t\t\t\t// Convert selectedIndex back to 1-based for display (oldest = 1)\n\t\t\t\tconst displayIndex = reversedSnapshots.length - this.selectedIndex;\n\t\t\t\tlines.push(theme.fg(\"muted\", `  (${displayIndex}/${snapshots.length} · newest first)`));\n\t\t\t}\n\t\t}\n\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\tlines.push(...this.renderHistoryActionHints(width));\n\n\t\treturn lines;\n\t}\n\n\tprivate renderHistorySnapshotLine(\n\t\tsnapshot: MemoryHistorySnapshot,\n\t\tisSelected: boolean,\n\t\twidth: number,\n\t\tchronologicalIndex: number,\n\t): string {\n\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \"  \";\n\t\tconst date = new Date(snapshot.recordedAt);\n\t\tconst ageLabel = this.getRelativeAgeLabel(date);\n\t\tconst itemCount = snapshot.items.length;\n\t\tconst itemLabel = itemCount === 1 ? \"item\" : \"items\";\n\t\tconst currentMarker = snapshot.isCurrent ? ` ${theme.fg(\"accent\", \"[current]\")}` : \"\";\n\t\tconst baselineMarker =\n\t\t\tthis.armedBaselineIndex === chronologicalIndex && this.compareStep !== \"target_picking\"\n\t\t\t\t? ` ${theme.fg(\"warning\", \"[baseline]\")}`\n\t\t\t\t: \"\";\n\t\tconst targetMarker =\n\t\t\tthis.compareStep === \"target_picking\" && isSelected && this.armedBaselineIndex !== chronologicalIndex\n\t\t\t\t? ` ${theme.fg(\"accent\", \"[target]\")}`\n\t\t\t\t: \"\";\n\n\t\tconst cursorOrIndicator = snapshot.isCurrent ? theme.fg(\"success\", \"●\") : \"○\";\n\t\tconst ageText = theme.bold(theme.fg(\"text\", ageLabel));\n\t\tconst dateText = theme.fg(\"dim\", date.toLocaleDateString());\n\n\t\tconst rawLine = `${cursor}${cursorOrIndicator} ${ageText}${currentMarker}${baselineMarker}${targetMarker} · ${itemCount} ${itemLabel} · ${dateText}`;\n\t\tconst truncated = truncateToWidth(rawLine, width, \"…\");\n\t\treturn isSelected ? theme.bg(\"selectedBg\", truncated) : truncated;\n\t}\n\n\tprivate renderHistoryDetailMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\n\t\tif (snapshots.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\"  No snapshots available.\", width, \"…\")));\n\t\t} else {\n\t\t\tconst snapshot = snapshots[this.selectedIndex];\n\t\t\tif (!snapshot) {\n\t\t\t\tlines.push(theme.fg(\"error\", truncateToWidth(\"  Invalid snapshot selection.\", width, \"…\")));\n\t\t\t} else {\n\t\t\t\tconst date = new Date(snapshot.recordedAt);\n\t\t\t\tconst ageLabel = this.getRelativeAgeLabel(date);\n\n\t\t\t\tconst isBaseline = this.armedBaselineIndex === this.selectedIndex;\n\t\t\t\tconst baselineMarker = isBaseline ? ` ${theme.fg(\"warning\", \"[baseline]\")}` : \"\";\n\t\t\t\tlines.push(\n\t\t\t\t\t`${theme.bold(theme.fg(\"text\", ageLabel))}${snapshot.isCurrent ? ` ${theme.fg(\"accent\", \"[current]\")}` : \"\"}${baselineMarker}`,\n\t\t\t\t);\n\t\t\t\tlines.push(theme.fg(\"dim\", `  ${date.toLocaleString()}`));\n\t\t\t\tlines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`  ${snapshot.items.length} item${snapshot.items.length === 1 ? \"\" : \"s\"} at this point`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tlines.push(\"\");\n\n\t\t\t\t// Show all items in this snapshot\n\t\t\t\tif (snapshot.items.length === 0) {\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"  (empty)\"));\n\t\t\t\t} else {\n\t\t\t\t\tfor (const item of snapshot.items) {\n\t\t\t\t\t\tconst keyText = theme.bold(theme.fg(\"text\", item.key));\n\t\t\t\t\t\tconst valuePreview =\n\t\t\t\t\t\t\titem.value.length > this.maxValuePreview * 2\n\t\t\t\t\t\t\t\t? `${item.value.slice(0, this.maxValuePreview * 2)}…`\n\t\t\t\t\t\t\t\t: item.value;\n\t\t\t\t\t\tconst valueText = theme.fg(\"muted\", valuePreview);\n\t\t\t\t\t\tlines.push(truncateToWidth(`  ${keyText}: ${valueText}`, width, \"…\"));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (snapshot.isCurrent) {\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"  Note: This is a snapshot of the current memory state.\"));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\tlines.push(...this.renderHistoryDetailActionHints(width));\n\n\t\treturn lines;\n\t}\n\n\tprivate renderHistoryActionHints(width: number): string[] {\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\tconst hasSnapshots = snapshots.length > 0;\n\t\tconst hints: string[] = [];\n\n\t\tif (this.compareStep === \"target_picking\") {\n\t\t\t// Explicit target-picking step: confirm target or cancel\n\t\t\thints.push(rawKeyHint(\"↑↓\", \"select target\"));\n\t\t\thints.push(rawKeyHint(\"Enter\", \"confirm target → diff\"));\n\t\t\thints.push(rawKeyHint(\"c\", \"clear baseline\"));\n\t\t\thints.push(keyHint(\"selectCancel\", \"cancel picking\"));\n\t\t} else {\n\t\t\thints.push(rawKeyHint(\"↑↓\", \"navigate snapshots\"));\n\t\t\tif (hasSnapshots) {\n\t\t\t\thints.push(rawKeyHint(\"Enter\", \"view detail\"));\n\t\t\t}\n\t\t\tif (this.armedBaselineIndex === null) {\n\t\t\t\thints.push(rawKeyHint(\"b\", \"arm baseline\"));\n\t\t\t} else {\n\t\t\t\thints.push(rawKeyHint(\"c\", \"clear baseline\"));\n\t\t\t\thints.push(rawKeyHint(\"d\", \"pick target → diff\"));\n\t\t\t}\n\t\t\thints.push(keyHint(\"selectCancel\", \"back to list\"));\n\t\t}\n\n\t\treturn [truncateToWidth(hints.join(\"  \"), width, \"…\")];\n\t}\n\n\tprivate renderHistoryDetailActionHints(width: number): string[] {\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\tconst hasMultiple = snapshots.length > 1;\n\t\tconst hasBaseline = this.armedBaselineIndex !== null;\n\t\tconst hints: string[] = [];\n\n\t\tif (this.compareStep === \"target_picking\") {\n\t\t\t// Explicit target-picking step: confirm target or cancel\n\t\t\thints.push(rawKeyHint(\"↑↓\", \"select target\"));\n\t\t\thints.push(rawKeyHint(\"Enter\", \"confirm target → diff\"));\n\t\t\thints.push(rawKeyHint(\"c\", \"clear baseline\"));\n\t\t\thints.push(keyHint(\"selectCancel\", \"cancel picking\"));\n\t\t} else {\n\t\t\thints.push(rawKeyHint(\"↑↓\", \"switch snapshot\"));\n\t\t\tif (hasMultiple) {\n\t\t\t\thints.push(rawKeyHint(\"←→\", \"prev/next snapshot\"));\n\t\t\t}\n\t\t\thints.push(rawKeyHint(\"b\", \"arm baseline\"));\n\t\t\tif (hasBaseline) {\n\t\t\t\thints.push(rawKeyHint(\"c\", \"clear baseline\"));\n\t\t\t\thints.push(rawKeyHint(\"d\", \"pick target → diff\"));\n\t\t\t} else {\n\t\t\t\thints.push(rawKeyHint(\"d\", \"diff vs previous\"));\n\t\t\t}\n\t\t\thints.push(keyHint(\"selectCancel\", \"back to history\"));\n\t\t}\n\n\t\treturn [truncateToWidth(hints.join(\"  \"), width, \"…\")];\n\t}\n\n\tprivate renderHistoryDiffMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\n\t\tif (snapshots.length === 0 || this.diffBaseIndex === null) {\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\"  No snapshots available.\", width, \"…\")));\n\t\t} else {\n\t\t\tconst baseSnapshot = snapshots[this.diffBaseIndex];\n\t\t\tconst targetSnapshot = snapshots[this.selectedIndex];\n\n\t\t\tif (!baseSnapshot || !targetSnapshot) {\n\t\t\t\tlines.push(theme.fg(\"error\", truncateToWidth(\"  Invalid snapshot selection.\", width, \"…\")));\n\t\t\t} else {\n\t\t\t\t// Header: identify both snapshots being compared\n\t\t\t\tconst baseDate = new Date(baseSnapshot.recordedAt);\n\t\t\t\tconst targetDate = new Date(targetSnapshot.recordedAt);\n\t\t\t\tconst isExplicitBaseline = this.armedBaselineIndex !== null;\n\t\t\t\tlines.push(\n\t\t\t\t\t`  ${theme.bold(theme.fg(\"text\", isExplicitBaseline ? \"Baseline:\" : \"Base:\"))} ${this.getRelativeAgeLabel(baseDate)} — ${theme.fg(\"dim\", baseDate.toLocaleString())}`,\n\t\t\t\t);\n\t\t\t\tlines.push(\n\t\t\t\t\t`  ${theme.bold(theme.fg(\"text\", \"Target:\"))} ${this.getRelativeAgeLabel(targetDate)} — ${theme.fg(\"dim\", targetDate.toLocaleString())}`,\n\t\t\t\t);\n\t\t\t\tlines.push(theme.fg(\"dim\", `  (snapshot comparison · not an event log)`));\n\t\t\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\n\t\t\t\t// Compute diff\n\t\t\t\tconst diff = computeMemorySnapshotDiff(baseSnapshot, targetSnapshot);\n\n\t\t\t\tif (diff.added.length > 0) {\n\t\t\t\t\tlines.push(theme.bold(theme.fg(\"success\", `  + Added (${diff.added.length})`)));\n\t\t\t\t\tfor (const item of diff.added.slice(0, this.maxVisible)) {\n\t\t\t\t\t\tconst preview =\n\t\t\t\t\t\t\titem.value.length > this.maxValuePreview * 2\n\t\t\t\t\t\t\t\t? `${item.value.slice(0, this.maxValuePreview * 2)}…`\n\t\t\t\t\t\t\t\t: item.value;\n\t\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\ttruncateToWidth(\n\t\t\t\t\t\t\t\t`  ${theme.fg(\"success\", \"+\")} ${theme.fg(\"accent\", item.key)}: ${theme.fg(\"muted\", preview)}`,\n\t\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\t\t\"…\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (diff.added.length > this.maxVisible) {\n\t\t\t\t\t\tlines.push(theme.fg(\"dim\", `  … and ${diff.added.length - this.maxVisible} more`));\n\t\t\t\t\t}\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\t\t\t}\n\n\t\t\t\tif (diff.removed.length > 0) {\n\t\t\t\t\tlines.push(theme.bold(theme.fg(\"error\", `  - Removed (${diff.removed.length})`)));\n\t\t\t\t\tfor (const item of diff.removed.slice(0, this.maxVisible)) {\n\t\t\t\t\t\tconst preview =\n\t\t\t\t\t\t\titem.value.length > this.maxValuePreview * 2\n\t\t\t\t\t\t\t\t? `${item.value.slice(0, this.maxValuePreview * 2)}…`\n\t\t\t\t\t\t\t\t: item.value;\n\t\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\ttruncateToWidth(\n\t\t\t\t\t\t\t\t`  ${theme.fg(\"error\", \"-\")} ${theme.fg(\"accent\", item.key)}: ${theme.fg(\"muted\", preview)}`,\n\t\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\t\t\"…\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (diff.removed.length > this.maxVisible) {\n\t\t\t\t\t\tlines.push(theme.fg(\"dim\", `  … and ${diff.removed.length - this.maxVisible} more`));\n\t\t\t\t\t}\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\t\t\t}\n\n\t\t\t\tif (diff.changed.length > 0) {\n\t\t\t\t\tlines.push(theme.bold(theme.fg(\"warning\", `  ~ Changed (${diff.changed.length})`)));\n\t\t\t\t\tfor (const item of diff.changed.slice(0, this.maxVisible)) {\n\t\t\t\t\t\tconst prevPreview =\n\t\t\t\t\t\t\titem.previousValue.length > this.maxValuePreview\n\t\t\t\t\t\t\t\t? `${item.previousValue.slice(0, this.maxValuePreview)}…`\n\t\t\t\t\t\t\t\t: item.previousValue;\n\t\t\t\t\t\tconst currPreview =\n\t\t\t\t\t\t\titem.currentValue.length > this.maxValuePreview\n\t\t\t\t\t\t\t\t? `${item.currentValue.slice(0, this.maxValuePreview)}…`\n\t\t\t\t\t\t\t\t: item.currentValue;\n\t\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\ttruncateToWidth(`  ${theme.fg(\"warning\", \"~\")} ${theme.fg(\"accent\", item.key)}:`, width, \"…\"),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\ttruncateToWidth(\n\t\t\t\t\t\t\t\t`    ${theme.fg(\"dim\", prevPreview)} → ${theme.fg(\"text\", currPreview)}`,\n\t\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\t\t\"…\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (diff.changed.length > this.maxVisible) {\n\t\t\t\t\t\tlines.push(theme.fg(\"dim\", `  … and ${diff.changed.length - this.maxVisible} more`));\n\t\t\t\t\t}\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\t\t\t}\n\n\t\t\t\tif (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"  No changes between snapshots.\"));\n\t\t\t\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlines.push(...this.renderHistoryDiffActionHints(width));\n\n\t\treturn lines;\n\t}\n\n\tprivate renderHistoryDiffActionHints(width: number): string[] {\n\t\tconst hints: string[] = [];\n\t\thints.push(rawKeyHint(\"↑↓\", \"navigate snapshots\"));\n\t\tif (this.armedBaselineIndex !== null) {\n\t\t\thints.push(rawKeyHint(\"c\", \"clear baseline\"));\n\t\t}\n\t\thints.push(keyHint(\"selectCancel\", \"back to detail\"));\n\t\treturn [truncateToWidth(hints.join(\"  \"), width, \"…\")];\n\t}\n\n\tprivate renderConfirmMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst isDelete = this.mode === \"confirm_delete\";\n\t\tconst itemKey = this.editingItemKey;\n\n\t\tconst itemText = isDelete && itemKey ? ` \"${itemKey}\"` : \"\";\n\t\tconst actionText = isDelete ? \"delete\" : \"clear all\";\n\t\tconst color: \"error\" | \"warning\" = \"error\";\n\n\t\tlines.push(\"\");\n\t\tlines.push(theme.fg(color, truncateToWidth(`  Confirm ${actionText}${itemText}?`, width, \"…\")));\n\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\"  This action cannot be undone.\", width, \"…\")));\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\ttruncateToWidth(\n\t\t\t\t[keyHint(\"selectConfirm\", \"confirm\"), \"  \", keyHint(\"selectCancel\", \"cancel\")].join(\"\"),\n\t\t\t\twidth,\n\t\t\t\t\"…\",\n\t\t\t),\n\t\t);\n\n\t\treturn lines;\n\t}\n\n\tprivate renderInputMode(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst isAdd = this.mode === \"add_key\" || this.mode === \"add_value\";\n\t\tconst isValue = this.mode === \"add_value\" || this.mode === \"edit_value\";\n\t\tconst actionLabel = isAdd ? \"Add\" : \"Edit\";\n\n\t\t// Title\n\t\tconst title = isValue ? `${actionLabel} Value` : `${actionLabel} Key`;\n\t\tlines.push(theme.bold(theme.fg(\"accent\", title)));\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\n\t\tif (isValue && this.editingItemKey) {\n\t\t\t// Show current key while editing value\n\t\t\tconst keyLabel = theme.fg(\"muted\", \"Key:\");\n\t\t\tconst keyValue = theme.fg(\"text\", this.editingItemKey);\n\t\t\tlines.push(truncateToWidth(`${keyLabel} ${keyValue}`, width, \"…\"));\n\t\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\t}\n\n\t\tif (!isValue) {\n\t\t\tlines.push(...this.keyInput.render(width));\n\t\t} else {\n\t\t\tlines.push(...this.valueInput.render(width));\n\t\t}\n\n\t\tlines.push(theme.fg(\"dim\", \"─\".repeat(width)));\n\t\tlines.push(\n\t\t\ttruncateToWidth(\n\t\t\t\t[keyHint(\"selectConfirm\", \"save\"), \"  \", keyHint(\"selectCancel\", \"cancel\")].join(\"\"),\n\t\t\t\twidth,\n\t\t\t\t\"…\",\n\t\t\t),\n\t\t);\n\n\t\treturn lines;\n\t}\n\n\tprivate renderActionHints(width: number): string[] {\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tconst hasSelection = items.length > 0;\n\t\tconst hints: string[] = [];\n\n\t\tif (this.mode === \"list\") {\n\t\t\thints.push(rawKeyHint(\"↑↓\", \"navigate\"));\n\t\t\tif (hasSelection) {\n\t\t\t\thints.push(rawKeyHint(\"Enter\", \"edit\"));\n\t\t\t\thints.push(rawKeyHint(\"Ctrl+D\", \"delete\"));\n\t\t\t}\n\t\t\thints.push(rawKeyHint(\"a\", \"add\"));\n\t\t\thints.push(rawKeyHint(\"r\", \"review\"));\n\t\t\thints.push(rawKeyHint(\"h\", \"history\"));\n\t\t\tif (hasSelection) {\n\t\t\t\thints.push(rawKeyHint(\"Ctrl+Shift+C\", \"clear all\"));\n\t\t\t}\n\t\t\thints.push(keyHint(\"selectCancel\", \"close\"));\n\t\t} else if (this.mode === \"review\") {\n\t\t\t// No additional hints - navigation hints are inline in review mode\n\t\t}\n\n\t\treturn [truncateToWidth(hints.join(\"  \"), width, \"…\")];\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Keyboard handling\n\t// -------------------------------------------------------------------------\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\n\t\tif (this.mode === \"add_key\" || this.mode === \"edit_key\") {\n\t\t\tif (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\t\tthis.onKeyInputSubmit();\n\t\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\t\tthis.cancelInput();\n\t\t\t} else {\n\t\t\t\tthis.keyInput.handleInput(keyData);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.mode === \"add_value\" || this.mode === \"edit_value\") {\n\t\t\tif (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\t\tthis.onValueInputSubmit();\n\t\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\t\tthis.cancelInput();\n\t\t\t} else {\n\t\t\t\tthis.valueInput.handleInput(keyData);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.mode === \"confirm_delete\" || this.mode === \"confirm_clear\") {\n\t\t\tif (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\t\tthis.confirmDestructiveAction();\n\t\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\t\tthis.mode = \"list\";\n\t\t\t\tthis.editingItemKey = null;\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.mode === \"review\") {\n\t\t\tthis.handleReviewInput(keyData);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.mode === \"history\") {\n\t\t\tthis.handleHistoryInput(keyData);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.mode === \"history_detail\") {\n\t\t\tthis.handleHistoryDetailInput(keyData);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.mode === \"history_diff\") {\n\t\t\tthis.handleHistoryDiffInput(keyData);\n\t\t\treturn;\n\t\t}\n\n\t\t// List mode\n\t\tthis.handleListInput(keyData);\n\t}\n\n\tprivate handleListInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tconst items = this.callbacks.getMemoryItems();\n\n\t\t// Navigation\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(items.length - 1, this.selectedIndex + 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(items.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Enter: edit selected item\n\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tif (items.length > 0) {\n\t\t\t\tconst item = items[this.selectedIndex]!;\n\t\t\t\tthis.startEdit(item);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+D: delete selected item\n\t\tif (kb.matches(keyData, \"deleteSession\")) {\n\t\t\tif (items.length > 0) {\n\t\t\t\tconst item = items[this.selectedIndex]!;\n\t\t\t\tthis.startDelete(item.key);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+Shift+C: clear all\n\t\tif (this.matchesCtrlShiftC(keyData)) {\n\t\t\tif (items.length > 0) {\n\t\t\t\tthis.startClearAll();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// a: add new item\n\t\tif (keyData === \"a\") {\n\t\t\tthis.startAdd();\n\t\t\treturn;\n\t\t}\n\n\t\t// r: review mode\n\t\tif (keyData === \"r\") {\n\t\t\tthis.enterReviewMode();\n\t\t\treturn;\n\t\t}\n\n\t\t// h: history mode\n\t\tif (keyData === \"h\") {\n\t\t\tthis.enterHistoryMode();\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape: close\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.requestRender(); // Will be dismissed by parent\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate handleReviewInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tconst reviewed = reviewMemoryItems([...items]);\n\t\tconst targetEntries =\n\t\t\tthis.reviewSection === \"stale\"\n\t\t\t\t? reviewed.filter((r) => r.reviewRecommended)\n\t\t\t\t: reviewed.filter((r) => !r.reviewRecommended);\n\n\t\t// Arrow left/right: switch section\n\t\tif (keyData === \"\\u001b[D]\" /* left arrow */ || keyData === \"\\u001b[C]\" /* right arrow */) {\n\t\t\tthis.reviewSection = this.reviewSection === \"stale\" ? \"fresh\" : \"stale\";\n\t\t\tthis.selectedIndex = 0;\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Navigation within current section\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(targetEntries.length - 1, this.selectedIndex + 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(targetEntries.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape: back to list\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.mode = \"list\";\n\t\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, items.length - 1));\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate handleHistoryInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\t// Snapshots are displayed newest first\n\t\tconst reversedSnapshots = [...snapshots].reverse();\n\n\t\t// Handle target-picking step\n\t\tif (this.compareStep === \"target_picking\") {\n\t\t\t// Navigation — allowed in target-picking\n\t\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\t\tthis.selectedIndex = Math.min(reversedSnapshots.length - 1, this.selectedIndex + 1);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\t\tthis.selectedIndex = Math.min(reversedSnapshots.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Enter: confirm target and open diff\n\t\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\t\tif (snapshots.length > 0 && this.armedBaselineIndex !== null) {\n\t\t\t\t\tthis.diffBaseIndex = this.armedBaselineIndex;\n\t\t\t\t\t// Convert reversed display index to chronological before entering diff mode\n\t\t\t\t\tthis.selectedIndex = snapshots.length - 1 - this.selectedIndex;\n\t\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\t\tthis.mode = \"history_diff\";\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 'd': same as Enter — confirm target and open diff\n\t\t\tif (keyData === \"d\") {\n\t\t\t\tif (snapshots.length > 0 && this.armedBaselineIndex !== null) {\n\t\t\t\t\tthis.diffBaseIndex = this.armedBaselineIndex;\n\t\t\t\t\t// Convert reversed display index to chronological before entering diff mode\n\t\t\t\t\tthis.selectedIndex = snapshots.length - 1 - this.selectedIndex;\n\t\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\t\tthis.mode = \"history_diff\";\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 'c': clear baseline and exit target-picking\n\t\t\tif (keyData === \"c\") {\n\t\t\t\tthis.armedBaselineIndex = null;\n\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Escape: exit target-picking, keep baseline armed\n\t\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Baseline-armed state (not in target-picking): d enters target-picking\n\n\t\t// Navigation\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(reversedSnapshots.length - 1, this.selectedIndex + 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(reversedSnapshots.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Enter: view snapshot detail\n\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tif (snapshots.length > 0) {\n\t\t\t\t// Convert from reversed index to original index\n\t\t\t\tconst originalIndex = snapshots.length - 1 - this.selectedIndex;\n\t\t\t\tthis.selectedIndex = originalIndex;\n\t\t\t\tthis.mode = \"history_detail\";\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// 'b': arm current snapshot as baseline\n\t\tif (keyData === \"b\") {\n\t\t\tif (snapshots.length > 0) {\n\t\t\t\tconst originalIndex = snapshots.length - 1 - this.selectedIndex;\n\t\t\t\tthis.armedBaselineIndex = originalIndex;\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// 'd': if baseline armed, enter explicit target-picking; otherwise adjacent diff\n\t\tif (keyData === \"d\") {\n\t\t\tif (snapshots.length < 2 && this.armedBaselineIndex === null) {\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.armedBaselineIndex !== null) {\n\t\t\t\t// Enter explicit target-picking step\n\t\t\t\tthis.compareStep = \"target_picking\";\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// No baseline: adjacent diff immediately (quick default)\n\t\t\tconst targetChronologicalIndex = snapshots.length - 1 - this.selectedIndex;\n\t\t\tthis.diffBaseIndex = targetChronologicalIndex > 0 ? targetChronologicalIndex - 1 : 0;\n\t\t\tthis.mode = \"history_diff\";\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// 'c': clear armed baseline\n\t\tif (keyData === \"c\") {\n\t\t\tthis.clearBaseline();\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape: back to list\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.mode = \"list\";\n\t\t\tthis.syncHistorySelection();\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate handleHistoryDetailInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\n\t\t// Handle target-picking step\n\t\tif (this.compareStep === \"target_picking\") {\n\t\t\t// Navigation — allowed in target-picking\n\t\t\tif (keyData === \"\\u001b[D]\" /* left arrow */) {\n\t\t\t\tif (this.selectedIndex > 0) {\n\t\t\t\t\tthis.selectedIndex--;\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (keyData === \"\\u001b[C]\" /* right arrow */) {\n\t\t\t\tif (this.selectedIndex < snapshots.length - 1) {\n\t\t\t\t\tthis.selectedIndex++;\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\t\tthis.selectedIndex = Math.min(snapshots.length - 1, this.selectedIndex + 1);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\t\tthis.selectedIndex = Math.min(snapshots.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Enter: confirm target and open diff\n\t\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\t\tif (this.armedBaselineIndex !== null) {\n\t\t\t\t\tthis.diffBaseIndex = this.armedBaselineIndex;\n\t\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\t\tthis.mode = \"history_diff\";\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 'd': same as Enter — confirm target and open diff\n\t\t\tif (keyData === \"d\") {\n\t\t\t\tif (this.armedBaselineIndex !== null) {\n\t\t\t\t\tthis.diffBaseIndex = this.armedBaselineIndex;\n\t\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\t\tthis.mode = \"history_diff\";\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 'c': clear baseline and exit target-picking\n\t\t\tif (keyData === \"c\") {\n\t\t\t\tthis.armedBaselineIndex = null;\n\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Escape: exit target-picking, keep baseline armed\n\t\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\t\tthis.compareStep = \"none\";\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Baseline-armed state (not in target-picking): d enters target-picking\n\n\t\t// Arrow left: previous snapshot\n\t\tif (keyData === \"\\u001b[D]\" /* left arrow */) {\n\t\t\tif (this.selectedIndex > 0) {\n\t\t\t\tthis.selectedIndex--;\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Arrow right: next snapshot\n\t\tif (keyData === \"\\u001b[C]\" /* right arrow */) {\n\t\t\tif (this.selectedIndex < snapshots.length - 1) {\n\t\t\t\tthis.selectedIndex++;\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Navigation\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(snapshots.length - 1, this.selectedIndex + 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(snapshots.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// 'd': if baseline armed, enter explicit target-picking; otherwise adjacent diff\n\t\tif (keyData === \"d\") {\n\t\t\tif (snapshots.length < 2 && this.armedBaselineIndex === null) {\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.armedBaselineIndex !== null) {\n\t\t\t\t// Enter explicit target-picking step\n\t\t\t\tthis.compareStep = \"target_picking\";\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// No baseline: adjacent diff immediately (quick default)\n\t\t\tthis.diffBaseIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : 0;\n\t\t\tthis.mode = \"history_diff\";\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// 'b': arm current snapshot as baseline\n\t\tif (keyData === \"b\") {\n\t\t\tif (snapshots.length > 0) {\n\t\t\t\tthis.armBaseline();\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// 'c': clear armed baseline\n\t\tif (keyData === \"c\") {\n\t\t\tthis.clearBaseline();\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape: back to history list\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.mode = \"history\";\n\t\t\tthis.syncHistorySelection();\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate handleHistoryDiffInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\n\t\t// Navigate to different target snapshot\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(snapshots.length - 1, this.selectedIndex + 1);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(snapshots.length - 1, this.selectedIndex + this.maxVisible);\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// 'c': clear armed baseline\n\t\tif (keyData === \"c\") {\n\t\t\tthis.clearBaseline();\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape: back to history detail\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.mode = \"history_detail\";\n\t\t\tthis.diffBaseIndex = null;\n\t\t\t// Keep armed baseline active so operator can resume comparison\n\t\t\tthis.requestRender();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate matchesCtrlShiftC(keyData: string): boolean {\n\t\t// Ctrl+Shift+C in Kitty/xterm-256color sends \"\\x1b[68;6u\"\n\t\t// Ctrl+Shift+D sends \"\\x1b[67;6u\" (used for clear all)\n\t\treturn keyData === \"\\x1b[68;6u\" || keyData === \"\\x1b[67;6u\";\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Actions\n\t// -------------------------------------------------------------------------\n\n\tprivate startAdd(): void {\n\t\tthis.mode = \"add_key\";\n\t\tthis.keyInput.setValue(\"\");\n\t\tthis.editingItemKey = null;\n\t\tthis.keyInput.focused = true;\n\t\tthis.requestRender();\n\t}\n\n\tprivate startEdit(item: MemoryItem): void {\n\t\tthis.mode = \"edit_key\";\n\t\tthis.keyInput.setValue(item.key);\n\t\tthis.editingItemKey = item.key;\n\t\tthis.valueInput.setValue(item.value);\n\t\tthis.keyInput.focused = true;\n\t\tthis.requestRender();\n\t}\n\n\tprivate startDelete(key: string): void {\n\t\tthis.mode = \"confirm_delete\";\n\t\tthis.editingItemKey = key;\n\t\tthis.requestRender();\n\t}\n\n\tprivate startClearAll(): void {\n\t\tthis.mode = \"confirm_clear\";\n\t\tthis.requestRender();\n\t}\n\n\tprivate onKeyInputSubmit(): void {\n\t\tconst key = this.keyInput.getValue().trim();\n\t\tif (!key) {\n\t\t\tthis.cancelInput();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for duplicate key in add mode\n\t\tconst existingItems = this.callbacks.getMemoryItems();\n\t\tif (this.mode === \"add_key\" && existingItems.some((item) => item.key === key)) {\n\t\t\t// Show error by briefly flashing the key input - just proceed to value input\n\t\t}\n\n\t\tif (this.mode === \"add_key\") {\n\t\t\tthis.mode = \"add_value\";\n\t\t\tthis.valueInput.setValue(\"\");\n\t\t\tthis.valueInput.focused = true;\n\t\t} else {\n\t\t\tthis.mode = \"edit_value\";\n\t\t\tthis.valueInput.focused = true;\n\t\t}\n\t\tthis.requestRender();\n\t}\n\n\tprivate onValueInputSubmit(): void {\n\t\tconst key = this.keyInput.getValue().trim();\n\t\tconst value = this.valueInput.getValue();\n\n\t\tif (!key) {\n\t\t\tthis.cancelInput();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.callbacks.setMemoryItem(key, value);\n\t\tthis.refresh();\n\t\tthis.mode = \"list\";\n\t\tthis.keyInput.setValue(\"\");\n\t\tthis.valueInput.setValue(\"\");\n\t\tthis.editingItemKey = null;\n\t\tthis.requestRender();\n\t}\n\n\tprivate cancelInput(): void {\n\t\tthis.mode = \"list\";\n\t\tthis.keyInput.setValue(\"\");\n\t\tthis.valueInput.setValue(\"\");\n\t\tthis.editingItemKey = null;\n\t\tthis.requestRender();\n\t}\n\n\tprivate confirmDestructiveAction(): void {\n\t\tif (this.mode === \"confirm_delete\") {\n\t\t\tconst key = this.editingItemKey;\n\t\t\tif (key) {\n\t\t\t\tthis.callbacks.deleteMemoryItem(key);\n\t\t\t}\n\t\t} else if (this.mode === \"confirm_clear\") {\n\t\t\tthis.callbacks.clearMemory();\n\t\t}\n\n\t\tthis.refresh();\n\t\tthis.mode = \"list\";\n\t\tthis.editingItemKey = null;\n\t\tthis.requestRender();\n\t}\n\n\tprivate enterReviewMode(): void {\n\t\tthis.mode = \"review\";\n\t\tthis.selectedIndex = 0;\n\t\tthis.syncReviewSection();\n\t\tthis.requestRender();\n\t}\n\n\tprivate syncReviewSection(): void {\n\t\tconst items = this.callbacks.getMemoryItems();\n\t\tconst reviewed = reviewMemoryItems([...items]);\n\t\tconst staleCount = reviewed.filter((r) => r.reviewRecommended).length;\n\t\tconst freshCount = reviewed.length - staleCount;\n\n\t\tif (this.reviewSection === \"stale\" && staleCount === 0) {\n\t\t\tthis.reviewSection = \"fresh\";\n\t\t} else if (this.reviewSection === \"fresh\" && freshCount === 0) {\n\t\t\tthis.reviewSection = \"stale\";\n\t\t}\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, reviewed.length - 1));\n\t}\n\n\tprivate enterHistoryMode(): void {\n\t\tthis.mode = \"history\";\n\t\tthis.selectedIndex = 0;\n\t\t// Sync selection to the current snapshot (last one in original order, first in reversed)\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\tif (snapshots.length > 0) {\n\t\t\t// Start at the most recent snapshot (index 0 in reversed display)\n\t\t\tthis.selectedIndex = 0;\n\t\t}\n\t\tthis.requestRender();\n\t}\n\n\t/**\n\t * Sync selectedIndex when entering history list mode from detail or returning to list.\n\t * Resets to show the current/most recent snapshot.\n\t */\n\tprivate syncHistorySelection(): void {\n\t\tconst snapshots = this.callbacks.getMemoryHistory();\n\t\tif (snapshots.length === 0) {\n\t\t\tthis.selectedIndex = 0;\n\t\t\treturn;\n\t\t}\n\t\t// In history list mode, we display newest first.\n\t\t// Find the index of the most recent snapshot (last in original order = index 0 in reversed)\n\t\t// If we have a valid selectedIndex from detail mode, keep it.\n\t\t// Otherwise, default to the most recent (index 0 in reversed).\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, snapshots.length - 1);\n\t}\n\n\t/**\n\t * Arm the currently selected snapshot as the explicit baseline.\n\t * Uses the current selectedIndex (in original chronological order).\n\t */\n\tprivate armBaseline(): void {\n\t\tthis.armedBaselineIndex = this.selectedIndex;\n\t}\n\n\t/**\n\t * Clear the armed baseline and exit any active compare chooser step.\n\t */\n\tprivate clearBaseline(): void {\n\t\tthis.armedBaselineIndex = null;\n\t\tthis.compareStep = \"none\";\n\t}\n}\n"]}