imsg-mcp 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ # [1.14.0](https://github.com/george43g/imsg-mcp/compare/v1.13.0...v1.14.0) (2026-07-23)
7
+
8
+
9
+ ### Features
10
+
11
+ * **tui:** settings panel for media-interpretation config ([56f0db0](https://github.com/george43g/imsg-mcp/commit/56f0db01361e558979346d236cfb29b7727a3dae))
12
+
6
13
  # [1.13.0](https://github.com/george43g/imsg-mcp/compare/v1.12.0...v1.13.0) (2026-07-22)
7
14
 
8
15
 
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { Command } from "commander";
6
6
  import { d as checkLocalAccess, f as formatAccessReport, I as IMPLEMENTED_TYPES, A as ANALYTIC_INFO, i as installShutdownHandlers, r as registerCleanup, l as looksLikeThreadSlug } from "./shutdown-DhEgpRwh.js";
7
- import { A as APP_VERSION, t as toYaml } from "./meta-CIffb4eN.js";
7
+ import { A as APP_VERSION, t as toYaml } from "./meta-BS_x0KRa.js";
8
8
  import { spawn } from "node:child_process";
9
9
  import { join, dirname } from "node:path";
10
10
  function distRoot() {
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import Database from "better-sqlite3";
12
12
  import { n as normalizedPhoneVariants, h as hasNativeModule, I as IMessageDB, r as rankFuzzy } from "./imessage-db-CPCdARwa.js";
13
13
  import { parseUserDate } from "./date-parse-DJXMfq3a.js";
14
14
  import { streamExport, ExportInterpretGuardError } from "./exportStream-DUPD0lbq.js";
15
- import { r as renderAnalyticText, A as APP_VERSION, a as APP_NAME } from "./meta-CIffb4eN.js";
15
+ import { r as renderAnalyticText, A as APP_VERSION, a as APP_NAME } from "./meta-BS_x0KRa.js";
16
16
  import { randomUUID } from "node:crypto";
17
17
  import { z } from "zod";
18
18
  import { a as applyInlineInterpretations, i as imageBlockFromFile, v as videoPosterFrame, m as mediaMetadata, b as refForAttachment, T as TRANSCRIBE_MAX_BYTES, g as getInterpretRuntime, t as transcriptSourceEnum, d as detectTranscriber } from "./media-intel-runtime-C4C2QsM_.js";
@@ -155,11 +155,11 @@ function yamlScalar(s) {
155
155
  return needsQuote ? `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"` : s;
156
156
  }
157
157
  const APP_NAME = "imsg-mcp";
158
- const APP_VERSION = "1.13.0";
158
+ const APP_VERSION = "1.14.0";
159
159
  export {
160
160
  APP_VERSION as A,
161
161
  APP_NAME as a,
162
162
  renderAnalyticText as r,
163
163
  toYaml as t
164
164
  };
165
- //# sourceMappingURL=meta-CIffb4eN.js.map
165
+ //# sourceMappingURL=meta-BS_x0KRa.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"meta-CIffb4eN.js","sources":["../src/analytics-render.ts","../src/meta.ts"],"sourcesContent":["/**\n * Human-readable text rendering for every chat_analytics type, shared by the\n * MCP tool output and the CLI/console. Keeping it in one place means the agent\n * (via the tool's text block) and a person (via `imsg analytics …`) see the\n * same summary, and adding an analytic only needs one renderer.\n */\n\nimport type {\n AnalyticType,\n DoubleTextResult,\n HeatmapResult,\n RelationshipScore,\n ResponseTimeStats,\n StreakResult,\n TapbackResult,\n WrappedResult,\n} from \"./analytics.js\";\n\n/** ms → compact human duration (e.g. 4m, 2h, 3d). */\nfunction humanDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"—\";\n const s = Math.round(ms / 1000);\n if (s < 60) return `${s}s`;\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h`;\n return `${Math.round(h / 24)}d`;\n}\n\nconst TOP = 20;\n\nfunction renderStreaks(rows: StreakResult[]): string {\n if (rows.length === 0) return \"(no streaks in window)\";\n return rows\n .slice(0, TOP)\n .map((r, i) => {\n const span =\n r.longestStreakStart && r.longestStreakEnd\n ? ` (${r.longestStreakStart}→${r.longestStreakEnd})`\n : \"\";\n const cur = r.currentStreakDays > 0 ? `, current ${r.currentStreakDays}d` : \"\";\n return `${String(i + 1).padStart(2)}. ${r.contact} — longest ${r.longestStreakDays}d${span}${cur}`;\n })\n .join(\"\\n\");\n}\n\nfunction renderDoubleTexts(rows: DoubleTextResult[]): string {\n if (rows.length === 0) return \"(no double-texts in window)\";\n return rows\n .slice(0, TOP)\n .map(\n (r, i) =>\n `${String(i + 1).padStart(2)}. ${r.contact} — you ${r.doubleTextsFromMe}×, them ${r.doubleTextsFromThem}×`,\n )\n .join(\"\\n\");\n}\n\nfunction renderResponseTimes(rows: ResponseTimeStats[]): string {\n if (rows.length === 0) return \"(no response-time data in window)\";\n return rows\n .slice(0, TOP)\n .map(\n (r, i) =>\n `${String(i + 1).padStart(2)}. ${r.contact} — median ${humanDuration(r.medianMs)}, p95 ${humanDuration(r.p95Ms)}, mean ${humanDuration(r.meanMs)} (n=${r.count})`,\n )\n .join(\"\\n\");\n}\n\nconst DOW = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst HEAT = \" ░▒▓█\";\n\nfunction renderHeatmap(result: HeatmapResult): string {\n if (result.total === 0) return \"(no activity in window)\";\n const max = Math.max(...result.grid.flat());\n const header = ` ${Array.from({ length: 24 }, (_, h) => String(h % 10)).join(\"\")}`;\n const rows = result.grid.map((row, dow) => {\n const cells = row\n .map((v) => {\n if (v === 0) return HEAT[0];\n const level = Math.min(HEAT.length - 1, 1 + Math.floor((v / max) * (HEAT.length - 2)));\n return HEAT[level];\n })\n .join(\"\");\n return `${DOW[dow]} ${cells}`;\n });\n return `${header}\\n${rows.join(\"\\n\")}\\n(${result.total} messages; each column = hour 0–23)`;\n}\n\nfunction renderTapbacks(rows: TapbackResult[]): string {\n if (rows.length === 0) return \"(no tapbacks in window)\";\n return rows\n .slice(0, TOP)\n .map((r, i) => {\n const parts = [\n r.heart && `❤️${r.heart}`,\n r.thumbsUp && `👍${r.thumbsUp}`,\n r.thumbsDown && `👎${r.thumbsDown}`,\n r.haha && `😂${r.haha}`,\n r.exclaim && `‼️${r.exclaim}`,\n r.question && `❓${r.question}`,\n r.emoji && `🙂${r.emoji}`,\n ].filter(Boolean);\n return `${String(i + 1).padStart(2)}. ${r.contact} — ${parts.join(\" \")} (total ${r.total})`;\n })\n .join(\"\\n\");\n}\n\nfunction renderWrapped(w: WrappedResult): string {\n const lines = [\n `Window: ${w.windowStart} → ${w.windowEnd}`,\n `Sent: ${w.totalSent.toLocaleString()} Received: ${w.totalReceived.toLocaleString()} Reactions: ${w.totalReactions.toLocaleString()}`,\n w.peakDay ? `Peak day: ${w.peakDay.date} (${w.peakDay.count} messages)` : \"Peak day: —\",\n w.longestStreakContact\n ? `Longest streak: ${w.longestStreakDays}d with ${w.longestStreakContact}`\n : `Longest streak: ${w.longestStreakDays}d`,\n \"\",\n \"Top contacts:\",\n ...w.topContacts\n .slice(0, 10)\n .map(\n (c, i) =>\n `${String(i + 1).padStart(2)}. ${c.contact} — ${c.total} (↑${c.sent} / ↓${c.received})`,\n ),\n ];\n return lines.join(\"\\n\");\n}\n\nfunction renderLeaderboard(rows: RelationshipScore[]): string {\n if (rows.length === 0) return \"(no ranked relationships in window)\";\n const max = rows[0]?.score || 0;\n return rows\n .slice(0, TOP)\n .map((r, i) => {\n const rel = max > 0 ? Math.round((r.score / max) * 100) : 0;\n return `${String(i + 1).padStart(2)}. ${r.contact} — ${r.total} msgs, ${Math.round(\n r.reciprocity * 100,\n )}% reciprocity, last ${Math.round(r.daysSinceLast)}d ago (score ${rel})`;\n })\n .join(\"\\n\");\n}\n\n/** Render any analytic's `data` payload as human-readable text. */\nexport function renderAnalyticText(type: AnalyticType, data: unknown): string {\n switch (type) {\n case \"messaging_streaks\":\n return renderStreaks(\n (data as { streaks?: StreakResult[] }).streaks ?? (data as StreakResult[]),\n );\n case \"double_texts\":\n return renderDoubleTexts(\n (data as { doubleTexts?: DoubleTextResult[] }).doubleTexts ?? (data as DoubleTextResult[]),\n );\n case \"response_time_stats\":\n return renderResponseTimes(\n (data as { responseTimes?: ResponseTimeStats[] }).responseTimes ??\n (data as ResponseTimeStats[]),\n );\n case \"daily_heatmap\":\n return renderHeatmap(data as HeatmapResult);\n case \"tapback_summary\":\n return renderTapbacks(\n (data as { tapbacks?: TapbackResult[] }).tapbacks ?? (data as TapbackResult[]),\n );\n case \"year_in_review_wrapped\":\n return renderWrapped(data as WrappedResult);\n case \"relationship_leaderboard\":\n return renderLeaderboard((data as { leaderboard?: RelationshipScore[] }).leaderboard ?? []);\n }\n}\n\n/**\n * Minimal YAML serializer for analytic output (`--yaml`). Zero-dep on purpose;\n * handles the shapes analytics produce — nested objects, arrays, strings,\n * numbers, booleans, null. Not a general-purpose emitter.\n */\nexport function toYaml(value: unknown, indent = 0): string {\n const pad = \" \".repeat(indent);\n if (value === null || value === undefined) return \"null\";\n if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n if (typeof value === \"string\") return yamlScalar(value);\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n return value\n .map((item) => {\n const rendered = toYaml(item, indent + 1);\n if (isComposite(item)) {\n // Nested block: put the first key inline after \"- \", rest indented.\n return `${pad}-\\n${rendered}`;\n }\n return `${pad}- ${rendered}`;\n })\n .join(\"\\n\");\n }\n if (typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>);\n if (entries.length === 0) return \"{}\";\n return entries\n .map(([k, v]) => {\n if (isComposite(v)) {\n return `${pad}${k}:\\n${toYaml(v, indent + 1)}`;\n }\n return `${pad}${k}: ${toYaml(v, indent)}`;\n })\n .join(\"\\n\");\n }\n return yamlScalar(String(value));\n}\n\nfunction isComposite(v: unknown): boolean {\n return typeof v === \"object\" && v !== null && (Array.isArray(v) ? v.length > 0 : true);\n}\n\nfunction yamlScalar(s: string): string {\n // Conservatively quote any string a YAML parser could misread as a non-string.\n // Critically this includes phone numbers (\"+15550000119\" would otherwise\n // parse as the integer 15550000119, dropping the \"+\") and keyword-ish values.\n const needsQuote =\n s === \"\" ||\n /^\\s|\\s$/.test(s) || // leading/trailing whitespace\n /[\\n\"\\\\]/.test(s) || // control / quote / backslash\n /^[-?:,[\\]{}&*!|>%@`#'\"]/.test(s) || // starts with a YAML indicator\n /:(\\s|$)|\\s#/.test(s) || // \": \" (mapping) or \" #\" (comment) in structural position\n /^[+-]?(\\d|\\.\\d)/.test(s) || // number-ish, incl. \"+61…\" phone numbers\n /^(true|false|yes|no|on|off|null|~)$/i.test(s) || // YAML keywords\n /^[+-]?\\.?(inf|nan)$/i.test(s);\n return needsQuote\n ? `\"${s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/\\n/g, \"\\\\n\")}\"`\n : s;\n}\n","// Injected by Vite `define` from package.json at build/test time.\ndeclare const __APP_VERSION__: string;\n\nexport const APP_NAME = \"imsg-mcp\";\n// Fallback covers running un-built source without Vite (e.g. plain tsx).\nexport const APP_VERSION = typeof __APP_VERSION__ !== \"undefined\" ? __APP_VERSION__ : \"0.0.0-dev\";\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"meta-BS_x0KRa.js","sources":["../src/analytics-render.ts","../src/meta.ts"],"sourcesContent":["/**\n * Human-readable text rendering for every chat_analytics type, shared by the\n * MCP tool output and the CLI/console. Keeping it in one place means the agent\n * (via the tool's text block) and a person (via `imsg analytics …`) see the\n * same summary, and adding an analytic only needs one renderer.\n */\n\nimport type {\n AnalyticType,\n DoubleTextResult,\n HeatmapResult,\n RelationshipScore,\n ResponseTimeStats,\n StreakResult,\n TapbackResult,\n WrappedResult,\n} from \"./analytics.js\";\n\n/** ms → compact human duration (e.g. 4m, 2h, 3d). */\nfunction humanDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"—\";\n const s = Math.round(ms / 1000);\n if (s < 60) return `${s}s`;\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h`;\n return `${Math.round(h / 24)}d`;\n}\n\nconst TOP = 20;\n\nfunction renderStreaks(rows: StreakResult[]): string {\n if (rows.length === 0) return \"(no streaks in window)\";\n return rows\n .slice(0, TOP)\n .map((r, i) => {\n const span =\n r.longestStreakStart && r.longestStreakEnd\n ? ` (${r.longestStreakStart}→${r.longestStreakEnd})`\n : \"\";\n const cur = r.currentStreakDays > 0 ? `, current ${r.currentStreakDays}d` : \"\";\n return `${String(i + 1).padStart(2)}. ${r.contact} — longest ${r.longestStreakDays}d${span}${cur}`;\n })\n .join(\"\\n\");\n}\n\nfunction renderDoubleTexts(rows: DoubleTextResult[]): string {\n if (rows.length === 0) return \"(no double-texts in window)\";\n return rows\n .slice(0, TOP)\n .map(\n (r, i) =>\n `${String(i + 1).padStart(2)}. ${r.contact} — you ${r.doubleTextsFromMe}×, them ${r.doubleTextsFromThem}×`,\n )\n .join(\"\\n\");\n}\n\nfunction renderResponseTimes(rows: ResponseTimeStats[]): string {\n if (rows.length === 0) return \"(no response-time data in window)\";\n return rows\n .slice(0, TOP)\n .map(\n (r, i) =>\n `${String(i + 1).padStart(2)}. ${r.contact} — median ${humanDuration(r.medianMs)}, p95 ${humanDuration(r.p95Ms)}, mean ${humanDuration(r.meanMs)} (n=${r.count})`,\n )\n .join(\"\\n\");\n}\n\nconst DOW = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst HEAT = \" ░▒▓█\";\n\nfunction renderHeatmap(result: HeatmapResult): string {\n if (result.total === 0) return \"(no activity in window)\";\n const max = Math.max(...result.grid.flat());\n const header = ` ${Array.from({ length: 24 }, (_, h) => String(h % 10)).join(\"\")}`;\n const rows = result.grid.map((row, dow) => {\n const cells = row\n .map((v) => {\n if (v === 0) return HEAT[0];\n const level = Math.min(HEAT.length - 1, 1 + Math.floor((v / max) * (HEAT.length - 2)));\n return HEAT[level];\n })\n .join(\"\");\n return `${DOW[dow]} ${cells}`;\n });\n return `${header}\\n${rows.join(\"\\n\")}\\n(${result.total} messages; each column = hour 0–23)`;\n}\n\nfunction renderTapbacks(rows: TapbackResult[]): string {\n if (rows.length === 0) return \"(no tapbacks in window)\";\n return rows\n .slice(0, TOP)\n .map((r, i) => {\n const parts = [\n r.heart && `❤️${r.heart}`,\n r.thumbsUp && `👍${r.thumbsUp}`,\n r.thumbsDown && `👎${r.thumbsDown}`,\n r.haha && `😂${r.haha}`,\n r.exclaim && `‼️${r.exclaim}`,\n r.question && `❓${r.question}`,\n r.emoji && `🙂${r.emoji}`,\n ].filter(Boolean);\n return `${String(i + 1).padStart(2)}. ${r.contact} — ${parts.join(\" \")} (total ${r.total})`;\n })\n .join(\"\\n\");\n}\n\nfunction renderWrapped(w: WrappedResult): string {\n const lines = [\n `Window: ${w.windowStart} → ${w.windowEnd}`,\n `Sent: ${w.totalSent.toLocaleString()} Received: ${w.totalReceived.toLocaleString()} Reactions: ${w.totalReactions.toLocaleString()}`,\n w.peakDay ? `Peak day: ${w.peakDay.date} (${w.peakDay.count} messages)` : \"Peak day: —\",\n w.longestStreakContact\n ? `Longest streak: ${w.longestStreakDays}d with ${w.longestStreakContact}`\n : `Longest streak: ${w.longestStreakDays}d`,\n \"\",\n \"Top contacts:\",\n ...w.topContacts\n .slice(0, 10)\n .map(\n (c, i) =>\n `${String(i + 1).padStart(2)}. ${c.contact} — ${c.total} (↑${c.sent} / ↓${c.received})`,\n ),\n ];\n return lines.join(\"\\n\");\n}\n\nfunction renderLeaderboard(rows: RelationshipScore[]): string {\n if (rows.length === 0) return \"(no ranked relationships in window)\";\n const max = rows[0]?.score || 0;\n return rows\n .slice(0, TOP)\n .map((r, i) => {\n const rel = max > 0 ? Math.round((r.score / max) * 100) : 0;\n return `${String(i + 1).padStart(2)}. ${r.contact} — ${r.total} msgs, ${Math.round(\n r.reciprocity * 100,\n )}% reciprocity, last ${Math.round(r.daysSinceLast)}d ago (score ${rel})`;\n })\n .join(\"\\n\");\n}\n\n/** Render any analytic's `data` payload as human-readable text. */\nexport function renderAnalyticText(type: AnalyticType, data: unknown): string {\n switch (type) {\n case \"messaging_streaks\":\n return renderStreaks(\n (data as { streaks?: StreakResult[] }).streaks ?? (data as StreakResult[]),\n );\n case \"double_texts\":\n return renderDoubleTexts(\n (data as { doubleTexts?: DoubleTextResult[] }).doubleTexts ?? (data as DoubleTextResult[]),\n );\n case \"response_time_stats\":\n return renderResponseTimes(\n (data as { responseTimes?: ResponseTimeStats[] }).responseTimes ??\n (data as ResponseTimeStats[]),\n );\n case \"daily_heatmap\":\n return renderHeatmap(data as HeatmapResult);\n case \"tapback_summary\":\n return renderTapbacks(\n (data as { tapbacks?: TapbackResult[] }).tapbacks ?? (data as TapbackResult[]),\n );\n case \"year_in_review_wrapped\":\n return renderWrapped(data as WrappedResult);\n case \"relationship_leaderboard\":\n return renderLeaderboard((data as { leaderboard?: RelationshipScore[] }).leaderboard ?? []);\n }\n}\n\n/**\n * Minimal YAML serializer for analytic output (`--yaml`). Zero-dep on purpose;\n * handles the shapes analytics produce — nested objects, arrays, strings,\n * numbers, booleans, null. Not a general-purpose emitter.\n */\nexport function toYaml(value: unknown, indent = 0): string {\n const pad = \" \".repeat(indent);\n if (value === null || value === undefined) return \"null\";\n if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n if (typeof value === \"string\") return yamlScalar(value);\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n return value\n .map((item) => {\n const rendered = toYaml(item, indent + 1);\n if (isComposite(item)) {\n // Nested block: put the first key inline after \"- \", rest indented.\n return `${pad}-\\n${rendered}`;\n }\n return `${pad}- ${rendered}`;\n })\n .join(\"\\n\");\n }\n if (typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>);\n if (entries.length === 0) return \"{}\";\n return entries\n .map(([k, v]) => {\n if (isComposite(v)) {\n return `${pad}${k}:\\n${toYaml(v, indent + 1)}`;\n }\n return `${pad}${k}: ${toYaml(v, indent)}`;\n })\n .join(\"\\n\");\n }\n return yamlScalar(String(value));\n}\n\nfunction isComposite(v: unknown): boolean {\n return typeof v === \"object\" && v !== null && (Array.isArray(v) ? v.length > 0 : true);\n}\n\nfunction yamlScalar(s: string): string {\n // Conservatively quote any string a YAML parser could misread as a non-string.\n // Critically this includes phone numbers (\"+15550000119\" would otherwise\n // parse as the integer 15550000119, dropping the \"+\") and keyword-ish values.\n const needsQuote =\n s === \"\" ||\n /^\\s|\\s$/.test(s) || // leading/trailing whitespace\n /[\\n\"\\\\]/.test(s) || // control / quote / backslash\n /^[-?:,[\\]{}&*!|>%@`#'\"]/.test(s) || // starts with a YAML indicator\n /:(\\s|$)|\\s#/.test(s) || // \": \" (mapping) or \" #\" (comment) in structural position\n /^[+-]?(\\d|\\.\\d)/.test(s) || // number-ish, incl. \"+61…\" phone numbers\n /^(true|false|yes|no|on|off|null|~)$/i.test(s) || // YAML keywords\n /^[+-]?\\.?(inf|nan)$/i.test(s);\n return needsQuote\n ? `\"${s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/\\n/g, \"\\\\n\")}\"`\n : s;\n}\n","// Injected by Vite `define` from package.json at build/test time.\ndeclare const __APP_VERSION__: string;\n\nexport const APP_NAME = \"imsg-mcp\";\n// Fallback covers running un-built source without Vite (e.g. plain tsx).\nexport const APP_VERSION = typeof __APP_VERSION__ !== \"undefined\" ? __APP_VERSION__ : \"0.0.0-dev\";\n"],"names":[],"mappings":"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"}
package/dist/tui.js CHANGED
@@ -4,7 +4,7 @@ import { parseArgs } from "node:util";
4
4
  import { useScreenSize, withFullScreen } from "fullscreen-ink";
5
5
  import { x as dispatchAnalytic, I as IMPLEMENTED_TYPES, g as getImsgDbPath, a as getContactsDbPaths, b as getSlugsDbPath, y as sendToChat, z as sendToChatId, B as sendMessageReliable, r as registerCleanup, d as checkLocalAccess, f as formatAccessReport, i as installShutdownHandlers, C as startHeapMonitor, D as logStartup, E as enableFileLogging, F as stopHeapMonitor, G as logShutdown } from "./shutdown-DhEgpRwh.js";
6
6
  import { spawnSync, execSync } from "node:child_process";
7
- import { r as resolveTuiConfig } from "./app-config-2yOG35Fr.js";
7
+ import { l as loadTuiConfig, I as InterpretConfigSchema, a as readCredentials, d as defaultTuiConfigPath, w as writeTuiConfig, r as resolveTuiConfig } from "./app-config-2yOG35Fr.js";
8
8
  import { o as onMemorySample, r as readWatchdogState, m as minMessageId, a as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog } from "./watchdog-C-aZkmhF.js";
9
9
  import { existsSync, copyFileSync, mkdirSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
@@ -862,6 +862,7 @@ const SIDEBAR_KEYS = [
862
862
  ["N", "new msg"],
863
863
  ["Tab", "→msgs"],
864
864
  ["/", "filter"],
865
+ [",", "settings"],
865
866
  ["d", "stats"],
866
867
  ["r", "refresh"],
867
868
  PALETTE_HINT,
@@ -940,6 +941,13 @@ const INFO_KEYS = [
940
941
  ["a", "export all"],
941
942
  ["Esc/q", "close"]
942
943
  ];
944
+ const SETTINGS_KEYS = [
945
+ ["j/k", "move"],
946
+ ["␣", "toggle"],
947
+ ["←/→", "adjust"],
948
+ ["K/J", "reorder"],
949
+ ["Esc/q", "close"]
950
+ ];
943
951
  function HelpBar({ mode, focus }) {
944
952
  const theme = useTheme();
945
953
  let keys;
@@ -948,6 +956,7 @@ function HelpBar({ mode, focus }) {
948
956
  else if (mode === "filter") keys = FILTER_KEYS;
949
957
  else if (mode === "drawer") keys = DRAWER_KEYS;
950
958
  else if (mode === "info") keys = INFO_KEYS;
959
+ else if (mode === "settings") keys = SETTINGS_KEYS;
951
960
  else if (mode === "select") keys = SELECT_KEYS;
952
961
  else if (mode === "export") keys = EXPORT_KEYS;
953
962
  else if (mode === "date-jump") keys = DATE_JUMP_KEYS;
@@ -1643,6 +1652,92 @@ function SendViaModal({ handle, apps }) {
1643
1652
  }
1644
1653
  );
1645
1654
  }
1655
+ function SettingsPanel({ rows, cursor, configPath, warnings, width, height }) {
1656
+ const theme = useTheme();
1657
+ const chromeH = 6;
1658
+ const bodyH = Math.max(4, height - chromeH);
1659
+ const start = Math.max(
1660
+ 0,
1661
+ Math.min(cursor - Math.floor(bodyH / 2), Math.max(0, rows.length - bodyH))
1662
+ );
1663
+ const end = Math.min(rows.length, start + bodyH);
1664
+ const visible = rows.slice(start, end);
1665
+ const labelW = Math.max(24, Math.floor(width * 0.5));
1666
+ return /* @__PURE__ */ jsxs(
1667
+ Box,
1668
+ {
1669
+ flexDirection: "column",
1670
+ width,
1671
+ height,
1672
+ borderStyle: "single",
1673
+ borderColor: theme.drawer.border,
1674
+ overflow: "hidden",
1675
+ children: [
1676
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, backgroundColor: theme.header.focused.bg, children: /* @__PURE__ */ jsx(Text, { color: theme.header.focused.fg, bold: true, children: "Settings — Media Interpretation" }) }),
1677
+ warnings.length > 0 && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.edited, wrap: "truncate", children: [
1678
+ "⚠ ",
1679
+ warnings[0]
1680
+ ] }) }),
1681
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: visible.map((row, i) => {
1682
+ const realIdx = start + i;
1683
+ const selected = realIdx === cursor;
1684
+ const prev = realIdx > 0 ? rows[realIdx - 1] : void 0;
1685
+ const showHeader = !prev || prev.section !== row.section;
1686
+ const key = `${row.section}-${row.kind}-${row.chain ?? ""}-${row.index ?? realIdx}`;
1687
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1688
+ showHeader && /* @__PURE__ */ jsx(Box, { marginTop: realIdx === 0 ? 0 : 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.help.desc, dimColor: true, children: [
1689
+ "── ",
1690
+ row.section,
1691
+ " ──"
1692
+ ] }) }),
1693
+ /* @__PURE__ */ jsxs(Box, { backgroundColor: selected ? theme.sidebar.selected : void 0, children: [
1694
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: selected ? theme.sidebar.selectedFg : theme.drawer.label, children: selected ? "▸ " : row.selectable ? " " : "· " }) }),
1695
+ /* @__PURE__ */ jsx(Box, { width: labelW, children: /* @__PURE__ */ jsx(
1696
+ Text,
1697
+ {
1698
+ color: selected ? theme.sidebar.selectedFg : row.selectable ? theme.drawer.value : theme.help.desc,
1699
+ bold: selected,
1700
+ wrap: "truncate",
1701
+ children: row.label
1702
+ }
1703
+ ) }),
1704
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(
1705
+ Text,
1706
+ {
1707
+ color: selected ? theme.sidebar.selectedFg : theme.info.value,
1708
+ bold: selected,
1709
+ wrap: "truncate",
1710
+ children: row.value
1711
+ }
1712
+ ) })
1713
+ ] }),
1714
+ selected && row.hint && /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: theme.help.desc, children: row.hint }) })
1715
+ ] }, key);
1716
+ }) }),
1717
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.help.desc, children: [
1718
+ /* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "j/k" }),
1719
+ " move",
1720
+ " ",
1721
+ /* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "␣" }),
1722
+ " toggle",
1723
+ " ",
1724
+ /* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "←/→" }),
1725
+ " adjust",
1726
+ " ",
1727
+ /* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "K/J" }),
1728
+ " reorder",
1729
+ " ",
1730
+ /* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "q" }),
1731
+ " close"
1732
+ ] }) }),
1733
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.help.desc, wrap: "truncate", children: [
1734
+ "Saved to ",
1735
+ configPath || "(default config path)"
1736
+ ] }) })
1737
+ ]
1738
+ }
1739
+ );
1740
+ }
1646
1741
  function matchesConversationFilter(c, queryLower) {
1647
1742
  return (c.displayName?.toLowerCase().includes(queryLower) ?? false) || c.chatIdentifier.toLowerCase().includes(queryLower) || c.threadSlug.toLowerCase().includes(queryLower);
1648
1743
  }
@@ -1655,6 +1750,212 @@ function firstFilterMatchIndex(conversations, query) {
1655
1750
  }
1656
1751
  return null;
1657
1752
  }
1753
+ const AUTO_ORDER = ["all", "free", "off"];
1754
+ const THRESHOLD_STEP = 5;
1755
+ function cap(s) {
1756
+ return s.length ? s[0].toUpperCase() + s.slice(1) : s;
1757
+ }
1758
+ function linkLabel(link) {
1759
+ if (link === "apple") return "Apple transcript (instant, free)";
1760
+ if (link === "local") return "Local tools (hear/yap/whisper)";
1761
+ if (link.startsWith("provider:")) return `Provider: ${link.slice("provider:".length)}`;
1762
+ return link;
1763
+ }
1764
+ function providerEnabled(interpret, name) {
1765
+ const tag = `provider:${name}`;
1766
+ return interpret.chains.audio.includes(tag) || interpret.chains.image.includes(tag) || interpret.chains.video.includes(tag);
1767
+ }
1768
+ function chainRows(interpret, chain) {
1769
+ const items = interpret.chains[chain];
1770
+ const section = `${cap(chain)} chain`;
1771
+ if (items.length === 0) {
1772
+ return [{ section, selectable: false, kind: "note", label: "(empty — no links)", value: "" }];
1773
+ }
1774
+ return items.map((item, i) => ({
1775
+ section,
1776
+ selectable: true,
1777
+ kind: "chain",
1778
+ chain,
1779
+ index: i,
1780
+ total: items.length,
1781
+ label: linkLabel(item),
1782
+ value: `#${i + 1}`,
1783
+ hint: "K/J reorder"
1784
+ }));
1785
+ }
1786
+ function buildSettingsRows(interpret, keyPresence) {
1787
+ const rows = [];
1788
+ rows.push({
1789
+ section: "General",
1790
+ selectable: true,
1791
+ kind: "auto",
1792
+ label: "Auto interpretation",
1793
+ value: interpret.auto,
1794
+ hint: "␣/←→ cycle (all · free · off)"
1795
+ });
1796
+ rows.push({
1797
+ section: "General",
1798
+ selectable: true,
1799
+ kind: "inline",
1800
+ label: "Inline transcripts in reads",
1801
+ value: interpret.inlineTranscripts ? "on" : "off",
1802
+ hint: "␣ toggle"
1803
+ });
1804
+ rows.push({
1805
+ section: "General",
1806
+ selectable: true,
1807
+ kind: "threshold",
1808
+ label: "Export cloud-confirm threshold",
1809
+ value: String(interpret.exportConfirmThreshold),
1810
+ hint: "←/→ ±5"
1811
+ });
1812
+ rows.push({
1813
+ section: "Sync nudge",
1814
+ selectable: true,
1815
+ kind: "nudge",
1816
+ label: "Attachment sync nudge",
1817
+ value: interpret.nudge.enabled ? "on" : "off",
1818
+ hint: "␣ toggle"
1819
+ });
1820
+ rows.push({
1821
+ section: "Sync nudge",
1822
+ selectable: true,
1823
+ kind: "nudge2",
1824
+ label: "Tier-2 'Sync Now' (Accessibility)",
1825
+ value: interpret.nudge.tier2SyncNow ? "on" : "off",
1826
+ hint: "␣ toggle"
1827
+ });
1828
+ rows.push(...chainRows(interpret, "audio"));
1829
+ rows.push(...chainRows(interpret, "image"));
1830
+ rows.push(...chainRows(interpret, "video"));
1831
+ if (interpret.providers.length === 0) {
1832
+ rows.push({
1833
+ section: "Providers",
1834
+ selectable: false,
1835
+ kind: "note",
1836
+ label: "None configured — run `imsg setup`",
1837
+ value: ""
1838
+ });
1839
+ } else {
1840
+ for (const p of interpret.providers) {
1841
+ const target = p.preset ?? p.baseUrl ?? "custom";
1842
+ const enabled = providerEnabled(interpret, p.name);
1843
+ const hasKey = Boolean(keyPresence[p.name]);
1844
+ rows.push({
1845
+ section: "Providers",
1846
+ selectable: false,
1847
+ kind: "provider",
1848
+ label: `${p.name} (${target})`,
1849
+ value: `${enabled ? "in chain" : "unused"} · ${hasKey ? "key set" : "no key"}`
1850
+ });
1851
+ }
1852
+ }
1853
+ return rows;
1854
+ }
1855
+ function firstSelectableIndex(rows) {
1856
+ const i = rows.findIndex((r) => r.selectable);
1857
+ return i < 0 ? 0 : i;
1858
+ }
1859
+ function lastSelectableIndex(rows) {
1860
+ for (let i = rows.length - 1; i >= 0; i--) {
1861
+ if (rows[i].selectable) return i;
1862
+ }
1863
+ return 0;
1864
+ }
1865
+ function stepSelectable(rows, from, dir) {
1866
+ let j = from + dir;
1867
+ while (j >= 0 && j < rows.length && !rows[j].selectable) j += dir;
1868
+ if (j < 0 || j >= rows.length) return from;
1869
+ return j;
1870
+ }
1871
+ function cycleAuto(cur, dir) {
1872
+ const idx = AUTO_ORDER.indexOf(cur);
1873
+ const at = idx < 0 ? 0 : idx;
1874
+ return AUTO_ORDER[(at + dir + AUTO_ORDER.length) % AUTO_ORDER.length];
1875
+ }
1876
+ function moveChain(interpret, chain, index, dir) {
1877
+ const arr = [...interpret.chains[chain]];
1878
+ const j = index + dir;
1879
+ if (j < 0 || j >= arr.length) return null;
1880
+ const tmp = arr[index];
1881
+ arr[index] = arr[j];
1882
+ arr[j] = tmp;
1883
+ return { ...interpret, chains: { ...interpret.chains, [chain]: arr } };
1884
+ }
1885
+ function applySettingsKey(interpret, row, action) {
1886
+ if (!row || !row.selectable) return null;
1887
+ switch (row.kind) {
1888
+ case "auto": {
1889
+ if (action === "left") return { ...interpret, auto: cycleAuto(interpret.auto, -1) };
1890
+ if (action === "right" || action === "toggle")
1891
+ return { ...interpret, auto: cycleAuto(interpret.auto, 1) };
1892
+ return null;
1893
+ }
1894
+ case "inline": {
1895
+ if (action === "toggle")
1896
+ return { ...interpret, inlineTranscripts: !interpret.inlineTranscripts };
1897
+ if (action === "left") return { ...interpret, inlineTranscripts: false };
1898
+ if (action === "right") return { ...interpret, inlineTranscripts: true };
1899
+ return null;
1900
+ }
1901
+ case "threshold": {
1902
+ if (action === "left")
1903
+ return {
1904
+ ...interpret,
1905
+ exportConfirmThreshold: Math.max(0, interpret.exportConfirmThreshold - THRESHOLD_STEP)
1906
+ };
1907
+ if (action === "right")
1908
+ return {
1909
+ ...interpret,
1910
+ exportConfirmThreshold: interpret.exportConfirmThreshold + THRESHOLD_STEP
1911
+ };
1912
+ return null;
1913
+ }
1914
+ case "nudge": {
1915
+ if (action === "toggle")
1916
+ return { ...interpret, nudge: { ...interpret.nudge, enabled: !interpret.nudge.enabled } };
1917
+ if (action === "left") return { ...interpret, nudge: { ...interpret.nudge, enabled: false } };
1918
+ if (action === "right") return { ...interpret, nudge: { ...interpret.nudge, enabled: true } };
1919
+ return null;
1920
+ }
1921
+ case "nudge2": {
1922
+ if (action === "toggle")
1923
+ return {
1924
+ ...interpret,
1925
+ nudge: { ...interpret.nudge, tier2SyncNow: !interpret.nudge.tier2SyncNow }
1926
+ };
1927
+ if (action === "left")
1928
+ return { ...interpret, nudge: { ...interpret.nudge, tier2SyncNow: false } };
1929
+ if (action === "right")
1930
+ return { ...interpret, nudge: { ...interpret.nudge, tier2SyncNow: true } };
1931
+ return null;
1932
+ }
1933
+ case "chain": {
1934
+ if (row.chain == null || row.index == null) return null;
1935
+ if (action === "moveUp") return moveChain(interpret, row.chain, row.index, -1);
1936
+ if (action === "moveDown") return moveChain(interpret, row.chain, row.index, 1);
1937
+ return null;
1938
+ }
1939
+ default:
1940
+ return null;
1941
+ }
1942
+ }
1943
+ function loadSettingsState() {
1944
+ const loaded = loadTuiConfig();
1945
+ const interpret = InterpretConfigSchema.parse(loaded.config.interpret ?? {});
1946
+ const creds = readCredentials();
1947
+ const keyPresence = {};
1948
+ for (const p of interpret.providers) keyPresence[p.name] = Boolean(creds[p.name]);
1949
+ return {
1950
+ interpret,
1951
+ keyPresence,
1952
+ configPath: loaded.source ?? defaultTuiConfigPath(),
1953
+ warnings: loaded.warnings
1954
+ };
1955
+ }
1956
+ function openSettings(dispatch) {
1957
+ dispatch({ type: "OPEN_SETTINGS", ...loadSettingsState() });
1958
+ }
1658
1959
  const hasSelection = (s) => s.selectedModuleIdx == null && s.conversations[s.selectedIdx] != null;
1659
1960
  const CORE_COMMANDS = [
1660
1961
  // ── Navigation ─────────────────────────────────────────────────────────
@@ -1928,6 +2229,14 @@ const CORE_COMMANDS = [
1928
2229
  description: "This window",
1929
2230
  run: ({ dispatch }) => dispatch({ type: "OPEN_PALETTE" })
1930
2231
  },
2232
+ {
2233
+ id: "core.settings",
2234
+ title: "Settings — media interpretation",
2235
+ category: "App",
2236
+ keybinding: ",",
2237
+ description: "Chains, toggles, providers",
2238
+ run: ({ dispatch }) => openSettings(dispatch)
2239
+ },
1931
2240
  { id: "core.quit", title: "Quit", category: "App", keybinding: "q / Ctrl-C", run: () => {
1932
2241
  } }
1933
2242
  ];
@@ -3651,7 +3960,12 @@ const initialState = {
3651
3960
  moduleInstances: [],
3652
3961
  selectedModuleIdx: null,
3653
3962
  paletteQuery: "",
3654
- paletteCursor: 0
3963
+ paletteCursor: 0,
3964
+ settingsInterpret: null,
3965
+ settingsKeyPresence: {},
3966
+ settingsCursor: 0,
3967
+ settingsConfigPath: "",
3968
+ settingsWarnings: []
3655
3969
  };
3656
3970
  function clampMsg(state, idx) {
3657
3971
  const total = state.messages.length;
@@ -3942,6 +4256,23 @@ function reducer(state, action) {
3942
4256
  return { ...state, paletteCursor: Math.max(0, state.paletteCursor + action.delta) };
3943
4257
  case "SET_PALETTE_CURSOR":
3944
4258
  return { ...state, paletteCursor: Math.max(0, action.index) };
4259
+ // ── Settings panel ─────────────────────────────────────────────────
4260
+ case "OPEN_SETTINGS":
4261
+ return {
4262
+ ...state,
4263
+ mode: "settings",
4264
+ settingsInterpret: action.interpret,
4265
+ settingsKeyPresence: action.keyPresence,
4266
+ settingsConfigPath: action.configPath,
4267
+ settingsWarnings: action.warnings,
4268
+ settingsCursor: 0
4269
+ };
4270
+ case "CLOSE_SETTINGS":
4271
+ return { ...state, mode: "browse" };
4272
+ case "SET_SETTINGS_CURSOR":
4273
+ return { ...state, settingsCursor: Math.max(0, action.index) };
4274
+ case "SET_SETTINGS_INTERPRET":
4275
+ return { ...state, settingsInterpret: action.interpret };
3945
4276
  // ── Module instances ───────────────────────────────────────────────
3946
4277
  case "OPEN_MODULE_INSTANCE": {
3947
4278
  const moduleInstances = [action.instance, ...state.moduleInstances];
@@ -4070,6 +4401,37 @@ function App() {
4070
4401
  });
4071
4402
  }
4072
4403
  }, []);
4404
+ const settingsRows = useMemo(
4405
+ () => state.settingsInterpret ? buildSettingsRows(state.settingsInterpret, state.settingsKeyPresence) : [],
4406
+ [state.settingsInterpret, state.settingsKeyPresence]
4407
+ );
4408
+ const applySettings = useCallback(
4409
+ (action) => {
4410
+ const cur = state.settingsInterpret;
4411
+ if (!cur) return;
4412
+ const row = settingsRows[state.settingsCursor];
4413
+ const next = applySettingsKey(cur, row, action);
4414
+ if (!next) return;
4415
+ try {
4416
+ const loaded = loadTuiConfig();
4417
+ writeTuiConfig({ ...loaded.config, interpret: next }, state.settingsConfigPath);
4418
+ dispatch({ type: "SET_SETTINGS_INTERPRET", interpret: next });
4419
+ if (action === "moveUp") {
4420
+ dispatch({ type: "SET_SETTINGS_CURSOR", index: Math.max(0, state.settingsCursor - 1) });
4421
+ } else if (action === "moveDown") {
4422
+ dispatch({ type: "SET_SETTINGS_CURSOR", index: state.settingsCursor + 1 });
4423
+ }
4424
+ dispatch({ type: "SET_STATUS", status: "Settings saved." });
4425
+ setTimeout(() => dispatch({ type: "SET_STATUS", status: "" }), 2e3);
4426
+ } catch (e) {
4427
+ dispatch({
4428
+ type: "SET_STATUS",
4429
+ status: `Settings write failed: ${e instanceof Error ? e.message : String(e)}`
4430
+ });
4431
+ }
4432
+ },
4433
+ [state.settingsInterpret, state.settingsCursor, state.settingsConfigPath, settingsRows]
4434
+ );
4073
4435
  const refreshAll = useCallback(async () => {
4074
4436
  dispatch({ type: "SET_LOADING", loading: true, status: "Refreshing..." });
4075
4437
  const convs = await imsg.loadConversations();
@@ -4353,6 +4715,36 @@ function App() {
4353
4715
  }
4354
4716
  return;
4355
4717
  }
4718
+ if (state.mode === "settings") {
4719
+ if (key.escape || input === "q") {
4720
+ dispatch({ type: "CLOSE_SETTINGS" });
4721
+ } else if (input === "j" || key.downArrow) {
4722
+ dispatch({
4723
+ type: "SET_SETTINGS_CURSOR",
4724
+ index: stepSelectable(settingsRows, state.settingsCursor, 1)
4725
+ });
4726
+ } else if (input === "k" || key.upArrow) {
4727
+ dispatch({
4728
+ type: "SET_SETTINGS_CURSOR",
4729
+ index: stepSelectable(settingsRows, state.settingsCursor, -1)
4730
+ });
4731
+ } else if (input === "g") {
4732
+ dispatch({ type: "SET_SETTINGS_CURSOR", index: firstSelectableIndex(settingsRows) });
4733
+ } else if (input === "G") {
4734
+ dispatch({ type: "SET_SETTINGS_CURSOR", index: lastSelectableIndex(settingsRows) });
4735
+ } else if (input === " " || key.return) {
4736
+ applySettings("toggle");
4737
+ } else if (input === "h" || key.leftArrow) {
4738
+ applySettings("left");
4739
+ } else if (input === "l" || key.rightArrow) {
4740
+ applySettings("right");
4741
+ } else if (input === "K") {
4742
+ applySettings("moveUp");
4743
+ } else if (input === "J") {
4744
+ applySettings("moveDown");
4745
+ }
4746
+ return;
4747
+ }
4356
4748
  if (state.mode === "date-jump") {
4357
4749
  if (key.escape) {
4358
4750
  dispatch({ type: "EXIT_DATE_JUMP" });
@@ -4436,6 +4828,10 @@ function App() {
4436
4828
  openInfoDrawer();
4437
4829
  return;
4438
4830
  }
4831
+ if (input === "," && !key.ctrl && !key.meta && state.mode === "browse") {
4832
+ openSettings(dispatch);
4833
+ return;
4834
+ }
4439
4835
  if (input === "V" && state.focus === "thread" && state.selectedMsgIdx >= 0) {
4440
4836
  dispatch({ type: "ENTER_SELECT_MODE" });
4441
4837
  return;
@@ -4769,7 +5165,17 @@ function App() {
4769
5165
  const selectedModuleDef = selectedModule ? findModule(selectedModule.moduleId) : void 0;
4770
5166
  const ModulePane = selectedModuleDef?.Pane;
4771
5167
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [
4772
- /* @__PURE__ */ jsxs(Box, { flexGrow: 1, height: bodyHeight, children: [
5168
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, height: bodyHeight, children: state.mode === "settings" ? /* @__PURE__ */ jsx(
5169
+ SettingsPanel,
5170
+ {
5171
+ rows: settingsRows,
5172
+ cursor: state.settingsCursor,
5173
+ configPath: state.settingsConfigPath,
5174
+ warnings: state.settingsWarnings,
5175
+ width: columns,
5176
+ height: bodyHeight
5177
+ }
5178
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
4773
5179
  /* @__PURE__ */ jsx(
4774
5180
  Sidebar,
4775
5181
  {
@@ -4844,7 +5250,7 @@ function App() {
4844
5250
  }
4845
5251
  ),
4846
5252
  state.showDevStats && /* @__PURE__ */ jsx(DevStats, { stats: devStats, width: devStatsWidth })
4847
- ] }),
5253
+ ] }) }),
4848
5254
  state.mode === "date-jump" && /* @__PURE__ */ jsx(
4849
5255
  DateJumpModal,
4850
5256
  {