@mariozechner/pi-coding-agent 0.49.3 → 0.50.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.
Files changed (207) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/README.md +310 -1230
  3. package/dist/cli/args.d.ts +5 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +57 -23
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/config-selector.d.ts +14 -0
  8. package/dist/cli/config-selector.d.ts.map +1 -0
  9. package/dist/cli/config-selector.js +31 -0
  10. package/dist/cli/config-selector.js.map +1 -0
  11. package/dist/cli/session-picker.d.ts.map +1 -1
  12. package/dist/cli/session-picker.js +1 -1
  13. package/dist/cli/session-picker.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +53 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +262 -67
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +8 -18
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +39 -55
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts.map +1 -1
  23. package/dist/core/bash-executor.js +2 -1
  24. package/dist/core/bash-executor.js.map +1 -1
  25. package/dist/core/diagnostics.d.ts +15 -0
  26. package/dist/core/diagnostics.d.ts.map +1 -0
  27. package/dist/core/diagnostics.js +2 -0
  28. package/dist/core/diagnostics.js.map +1 -0
  29. package/dist/core/export-html/template.css +9 -0
  30. package/dist/core/export-html/template.js +6 -4
  31. package/dist/core/extensions/index.d.ts +1 -1
  32. package/dist/core/extensions/index.d.ts.map +1 -1
  33. package/dist/core/extensions/index.js.map +1 -1
  34. package/dist/core/extensions/loader.d.ts +1 -1
  35. package/dist/core/extensions/loader.d.ts.map +1 -1
  36. package/dist/core/extensions/loader.js +10 -1
  37. package/dist/core/extensions/loader.js.map +1 -1
  38. package/dist/core/extensions/runner.d.ts +9 -3
  39. package/dist/core/extensions/runner.d.ts.map +1 -1
  40. package/dist/core/extensions/runner.js +39 -12
  41. package/dist/core/extensions/runner.js.map +1 -1
  42. package/dist/core/extensions/types.d.ts +112 -1
  43. package/dist/core/extensions/types.d.ts.map +1 -1
  44. package/dist/core/extensions/types.js.map +1 -1
  45. package/dist/core/footer-data-provider.d.ts +9 -2
  46. package/dist/core/footer-data-provider.d.ts.map +1 -1
  47. package/dist/core/footer-data-provider.js +13 -0
  48. package/dist/core/footer-data-provider.js.map +1 -1
  49. package/dist/core/model-registry.d.ts +42 -2
  50. package/dist/core/model-registry.d.ts.map +1 -1
  51. package/dist/core/model-registry.js +154 -44
  52. package/dist/core/model-registry.js.map +1 -1
  53. package/dist/core/model-resolver.d.ts.map +1 -1
  54. package/dist/core/model-resolver.js +3 -2
  55. package/dist/core/model-resolver.js.map +1 -1
  56. package/dist/core/package-manager.d.ts +129 -0
  57. package/dist/core/package-manager.d.ts.map +1 -0
  58. package/dist/core/package-manager.js +1148 -0
  59. package/dist/core/package-manager.js.map +1 -0
  60. package/dist/core/prompt-templates.d.ts +6 -0
  61. package/dist/core/prompt-templates.d.ts.map +1 -1
  62. package/dist/core/prompt-templates.js +114 -54
  63. package/dist/core/prompt-templates.js.map +1 -1
  64. package/dist/core/resource-loader.d.ts +160 -0
  65. package/dist/core/resource-loader.d.ts.map +1 -0
  66. package/dist/core/resource-loader.js +604 -0
  67. package/dist/core/resource-loader.js.map +1 -0
  68. package/dist/core/sdk.d.ts +14 -105
  69. package/dist/core/sdk.d.ts.map +1 -1
  70. package/dist/core/sdk.js +52 -304
  71. package/dist/core/sdk.js.map +1 -1
  72. package/dist/core/session-manager.d.ts.map +1 -1
  73. package/dist/core/session-manager.js +45 -1
  74. package/dist/core/session-manager.js.map +1 -1
  75. package/dist/core/settings-manager.d.ts +34 -16
  76. package/dist/core/settings-manager.d.ts.map +1 -1
  77. package/dist/core/settings-manager.js +104 -25
  78. package/dist/core/settings-manager.js.map +1 -1
  79. package/dist/core/skills.d.ts +18 -10
  80. package/dist/core/skills.d.ts.map +1 -1
  81. package/dist/core/skills.js +126 -93
  82. package/dist/core/skills.js.map +1 -1
  83. package/dist/core/system-prompt.d.ts +3 -27
  84. package/dist/core/system-prompt.d.ts.map +1 -1
  85. package/dist/core/system-prompt.js +16 -103
  86. package/dist/core/system-prompt.js.map +1 -1
  87. package/dist/core/tools/bash.d.ts.map +1 -1
  88. package/dist/core/tools/bash.js +2 -1
  89. package/dist/core/tools/bash.js.map +1 -1
  90. package/dist/core/tools/read.d.ts.map +1 -1
  91. package/dist/core/tools/read.js +4 -4
  92. package/dist/core/tools/read.js.map +1 -1
  93. package/dist/index.d.ts +12 -7
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +8 -6
  96. package/dist/index.js.map +1 -1
  97. package/dist/main.d.ts.map +1 -1
  98. package/dist/main.js +209 -97
  99. package/dist/main.js.map +1 -1
  100. package/dist/modes/interactive/components/bordered-loader.d.ts +5 -1
  101. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/bordered-loader.js +29 -9
  103. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  104. package/dist/modes/interactive/components/config-selector.d.ts +71 -0
  105. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -0
  106. package/dist/modes/interactive/components/config-selector.js +468 -0
  107. package/dist/modes/interactive/components/config-selector.js.map +1 -0
  108. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/footer.js +4 -0
  110. package/dist/modes/interactive/components/footer.js.map +1 -1
  111. package/dist/modes/interactive/components/index.d.ts +1 -0
  112. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/index.js +1 -0
  114. package/dist/modes/interactive/components/index.js.map +1 -1
  115. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  116. package/dist/modes/interactive/components/oauth-selector.js +3 -4
  117. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  118. package/dist/modes/interactive/components/session-selector.d.ts +18 -1
  119. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/session-selector.js +195 -87
  121. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  122. package/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
  123. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -0
  124. package/dist/modes/interactive/components/skill-invocation-message.js +47 -0
  125. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -0
  126. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/tool-execution.js +5 -5
  128. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  129. package/dist/modes/interactive/interactive-mode.d.ts +42 -2
  130. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  131. package/dist/modes/interactive/interactive-mode.js +535 -200
  132. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  133. package/dist/modes/interactive/theme/dark.json +1 -1
  134. package/dist/modes/interactive/theme/light.json +1 -1
  135. package/dist/modes/interactive/theme/theme-schema.json +8 -1
  136. package/dist/modes/interactive/theme/theme.d.ts +8 -1
  137. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  138. package/dist/modes/interactive/theme/theme.js +72 -25
  139. package/dist/modes/interactive/theme/theme.js.map +1 -1
  140. package/dist/modes/print-mode.d.ts.map +1 -1
  141. package/dist/modes/print-mode.js +25 -89
  142. package/dist/modes/print-mode.js.map +1 -1
  143. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  144. package/dist/modes/rpc/rpc-mode.js +32 -92
  145. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  146. package/dist/utils/git.d.ts +2 -0
  147. package/dist/utils/git.d.ts.map +1 -0
  148. package/dist/utils/git.js +6 -0
  149. package/dist/utils/git.js.map +1 -0
  150. package/dist/utils/shell.d.ts +1 -0
  151. package/dist/utils/shell.d.ts.map +1 -1
  152. package/dist/utils/shell.js +14 -1
  153. package/dist/utils/shell.js.map +1 -1
  154. package/dist/utils/sleep.d.ts +5 -0
  155. package/dist/utils/sleep.d.ts.map +1 -0
  156. package/dist/utils/sleep.js +17 -0
  157. package/dist/utils/sleep.js.map +1 -0
  158. package/docs/compaction.md +23 -21
  159. package/docs/custom-provider.md +538 -0
  160. package/docs/development.md +69 -0
  161. package/docs/extensions.md +180 -118
  162. package/docs/images/doom-extension.png +0 -0
  163. package/docs/images/interactive-mode.png +0 -0
  164. package/docs/images/tree-view.png +0 -0
  165. package/docs/json.md +79 -0
  166. package/docs/keybindings.md +162 -0
  167. package/docs/models.md +193 -0
  168. package/docs/packages.md +163 -0
  169. package/docs/prompt-templates.md +67 -0
  170. package/docs/providers.md +147 -0
  171. package/docs/sdk.md +111 -178
  172. package/docs/session.md +167 -16
  173. package/docs/settings.md +216 -0
  174. package/docs/shell-aliases.md +13 -0
  175. package/docs/skills.md +111 -202
  176. package/docs/terminal-setup.md +65 -0
  177. package/docs/themes.md +295 -0
  178. package/docs/tui.md +36 -5
  179. package/docs/windows.md +17 -0
  180. package/examples/README.md +1 -0
  181. package/examples/extensions/README.md +22 -2
  182. package/examples/extensions/bookmark.ts +50 -0
  183. package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  184. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  185. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  186. package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
  187. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  188. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
  189. package/examples/extensions/doom-overlay/doom/build.sh +1 -1
  190. package/examples/extensions/event-bus.ts +43 -0
  191. package/examples/extensions/message-renderer.ts +59 -0
  192. package/examples/extensions/session-name.ts +27 -0
  193. package/examples/extensions/with-deps/package-lock.json +2 -2
  194. package/examples/extensions/with-deps/package.json +1 -1
  195. package/examples/sdk/02-custom-model.ts +3 -3
  196. package/examples/sdk/03-custom-prompt.ts +20 -9
  197. package/examples/sdk/04-skills.ts +26 -27
  198. package/examples/sdk/06-extensions.ts +15 -6
  199. package/examples/sdk/07-context-files.ts +22 -18
  200. package/examples/sdk/08-prompt-templates.ts +19 -14
  201. package/examples/sdk/09-api-keys-and-oauth.ts +5 -12
  202. package/examples/sdk/10-settings.ts +3 -3
  203. package/examples/sdk/12-full-control.ts +16 -7
  204. package/examples/sdk/README.md +24 -30
  205. package/package.json +4 -4
  206. package/docs/theme.md +0 -617
  207. package/examples/extensions/chalk-logger.ts +0 -26
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@mariozechner/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts (similar to web-ui)\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tif (pwd.length > width) {\n\t\t\tconst half = Math.floor(width / 2) - 2;\n\t\t\tif (half > 0) {\n\t\t\t\tconst start = pwd.slice(0, half);\n\t\t\t\tconst end = pwd.slice(-(half - 1));\n\t\t\t\tpwd = `${start}...${end}`;\n\t\t\t} else {\n\t\t\t\tpwd = pwd.slice(0, Math.max(1, width));\n\t\t\t}\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Truncate statsLeft to fit width (no room for right side)\n\t\t\tconst plainStatsLeft = statsLeft.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\tstatsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst lines = [theme.fg(\"dim\", pwd), dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@mariozechner/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts (similar to web-ui)\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tif (pwd.length > width) {\n\t\t\tconst half = Math.floor(width / 2) - 2;\n\t\t\tif (half > 0) {\n\t\t\t\tconst start = pwd.slice(0, half);\n\t\t\t\tconst end = pwd.slice(-(half - 1));\n\t\t\t\tpwd = `${start}...${end}`;\n\t\t\t} else {\n\t\t\t\tpwd = pwd.slice(0, Math.max(1, width));\n\t\t\t}\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\t// Prepend the provider in parenthesis to the right side if there's multiple providers\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model.provider}) ${rightSide}`;\n\t\t}\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Truncate statsLeft to fit width (no room for right side)\n\t\t\tconst plainStatsLeft = statsLeft.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\tstatsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst lines = [theme.fg(\"dim\", pwd), dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -20,6 +20,7 @@ export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent
20
20
  export { SessionSelectorComponent } from "./session-selector.js";
21
21
  export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.js";
22
22
  export { ShowImagesSelectorComponent } from "./show-images-selector.js";
23
+ export { SkillInvocationMessageComponent } from "./skill-invocation-message.js";
23
24
  export { ThemeSelectorComponent } from "./theme-selector.js";
24
25
  export { ThinkingSelectorComponent } from "./thinking-selector.js";
25
26
  export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,iCAAiC,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACrH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC","sourcesContent":["// UI Components for extensions\nexport { ArminComponent } from \"./armin.js\";\nexport { AssistantMessageComponent } from \"./assistant-message.js\";\nexport { BashExecutionComponent } from \"./bash-execution.js\";\nexport { BorderedLoader } from \"./bordered-loader.js\";\nexport { BranchSummaryMessageComponent } from \"./branch-summary-message.js\";\nexport { CompactionSummaryMessageComponent } from \"./compaction-summary-message.js\";\nexport { CustomEditor } from \"./custom-editor.js\";\nexport { CustomMessageComponent } from \"./custom-message.js\";\nexport { type RenderDiffOptions, renderDiff } from \"./diff.js\";\nexport { DynamicBorder } from \"./dynamic-border.js\";\nexport { ExtensionEditorComponent } from \"./extension-editor.js\";\nexport { ExtensionInputComponent } from \"./extension-input.js\";\nexport { ExtensionSelectorComponent } from \"./extension-selector.js\";\nexport { FooterComponent } from \"./footer.js\";\nexport { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from \"./keybinding-hints.js\";\nexport { LoginDialogComponent } from \"./login-dialog.js\";\nexport { ModelSelectorComponent } from \"./model-selector.js\";\nexport { OAuthSelectorComponent } from \"./oauth-selector.js\";\nexport { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from \"./scoped-models-selector.js\";\nexport { SessionSelectorComponent } from \"./session-selector.js\";\nexport { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from \"./settings-selector.js\";\nexport { ShowImagesSelectorComponent } from \"./show-images-selector.js\";\nexport { ThemeSelectorComponent } from \"./theme-selector.js\";\nexport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nexport { ToolExecutionComponent, type ToolExecutionOptions } from \"./tool-execution.js\";\nexport { TreeSelectorComponent } from \"./tree-selector.js\";\nexport { UserMessageComponent } from \"./user-message.js\";\nexport { UserMessageSelectorComponent } from \"./user-message-selector.js\";\nexport { truncateToVisualLines, type VisualTruncateResult } from \"./visual-truncate.js\";\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,iCAAiC,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACrH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,+BAA+B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC","sourcesContent":["// UI Components for extensions\nexport { ArminComponent } from \"./armin.js\";\nexport { AssistantMessageComponent } from \"./assistant-message.js\";\nexport { BashExecutionComponent } from \"./bash-execution.js\";\nexport { BorderedLoader } from \"./bordered-loader.js\";\nexport { BranchSummaryMessageComponent } from \"./branch-summary-message.js\";\nexport { CompactionSummaryMessageComponent } from \"./compaction-summary-message.js\";\nexport { CustomEditor } from \"./custom-editor.js\";\nexport { CustomMessageComponent } from \"./custom-message.js\";\nexport { type RenderDiffOptions, renderDiff } from \"./diff.js\";\nexport { DynamicBorder } from \"./dynamic-border.js\";\nexport { ExtensionEditorComponent } from \"./extension-editor.js\";\nexport { ExtensionInputComponent } from \"./extension-input.js\";\nexport { ExtensionSelectorComponent } from \"./extension-selector.js\";\nexport { FooterComponent } from \"./footer.js\";\nexport { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from \"./keybinding-hints.js\";\nexport { LoginDialogComponent } from \"./login-dialog.js\";\nexport { ModelSelectorComponent } from \"./model-selector.js\";\nexport { OAuthSelectorComponent } from \"./oauth-selector.js\";\nexport { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from \"./scoped-models-selector.js\";\nexport { SessionSelectorComponent } from \"./session-selector.js\";\nexport { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from \"./settings-selector.js\";\nexport { ShowImagesSelectorComponent } from \"./show-images-selector.js\";\nexport { SkillInvocationMessageComponent } from \"./skill-invocation-message.js\";\nexport { ThemeSelectorComponent } from \"./theme-selector.js\";\nexport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nexport { ToolExecutionComponent, type ToolExecutionOptions } from \"./tool-execution.js\";\nexport { TreeSelectorComponent } from \"./tree-selector.js\";\nexport { UserMessageComponent } from \"./user-message.js\";\nexport { UserMessageSelectorComponent } from \"./user-message-selector.js\";\nexport { truncateToVisualLines, type VisualTruncateResult } from \"./visual-truncate.js\";\n"]}
@@ -21,6 +21,7 @@ export { ScopedModelsSelectorComponent } from "./scoped-models-selector.js";
21
21
  export { SessionSelectorComponent } from "./session-selector.js";
22
22
  export { SettingsSelectorComponent } from "./settings-selector.js";
23
23
  export { ShowImagesSelectorComponent } from "./show-images-selector.js";
24
+ export { SkillInvocationMessageComponent } from "./skill-invocation-message.js";
24
25
  export { ThemeSelectorComponent } from "./theme-selector.js";
25
26
  export { ThinkingSelectorComponent } from "./thinking-selector.js";
26
27
  export { ToolExecutionComponent } from "./tool-execution.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,iCAAiC,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAA0B,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAA2C,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACrH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAA+C,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAA6B,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAA6B,MAAM,sBAAsB,CAAC","sourcesContent":["// UI Components for extensions\nexport { ArminComponent } from \"./armin.js\";\nexport { AssistantMessageComponent } from \"./assistant-message.js\";\nexport { BashExecutionComponent } from \"./bash-execution.js\";\nexport { BorderedLoader } from \"./bordered-loader.js\";\nexport { BranchSummaryMessageComponent } from \"./branch-summary-message.js\";\nexport { CompactionSummaryMessageComponent } from \"./compaction-summary-message.js\";\nexport { CustomEditor } from \"./custom-editor.js\";\nexport { CustomMessageComponent } from \"./custom-message.js\";\nexport { type RenderDiffOptions, renderDiff } from \"./diff.js\";\nexport { DynamicBorder } from \"./dynamic-border.js\";\nexport { ExtensionEditorComponent } from \"./extension-editor.js\";\nexport { ExtensionInputComponent } from \"./extension-input.js\";\nexport { ExtensionSelectorComponent } from \"./extension-selector.js\";\nexport { FooterComponent } from \"./footer.js\";\nexport { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from \"./keybinding-hints.js\";\nexport { LoginDialogComponent } from \"./login-dialog.js\";\nexport { ModelSelectorComponent } from \"./model-selector.js\";\nexport { OAuthSelectorComponent } from \"./oauth-selector.js\";\nexport { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from \"./scoped-models-selector.js\";\nexport { SessionSelectorComponent } from \"./session-selector.js\";\nexport { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from \"./settings-selector.js\";\nexport { ShowImagesSelectorComponent } from \"./show-images-selector.js\";\nexport { ThemeSelectorComponent } from \"./theme-selector.js\";\nexport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nexport { ToolExecutionComponent, type ToolExecutionOptions } from \"./tool-execution.js\";\nexport { TreeSelectorComponent } from \"./tree-selector.js\";\nexport { UserMessageComponent } from \"./user-message.js\";\nexport { UserMessageSelectorComponent } from \"./user-message-selector.js\";\nexport { truncateToVisualLines, type VisualTruncateResult } from \"./visual-truncate.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,iCAAiC,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAA0B,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAA2C,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACrH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAA+C,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,+BAA+B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAA6B,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAA6B,MAAM,sBAAsB,CAAC","sourcesContent":["// UI Components for extensions\nexport { ArminComponent } from \"./armin.js\";\nexport { AssistantMessageComponent } from \"./assistant-message.js\";\nexport { BashExecutionComponent } from \"./bash-execution.js\";\nexport { BorderedLoader } from \"./bordered-loader.js\";\nexport { BranchSummaryMessageComponent } from \"./branch-summary-message.js\";\nexport { CompactionSummaryMessageComponent } from \"./compaction-summary-message.js\";\nexport { CustomEditor } from \"./custom-editor.js\";\nexport { CustomMessageComponent } from \"./custom-message.js\";\nexport { type RenderDiffOptions, renderDiff } from \"./diff.js\";\nexport { DynamicBorder } from \"./dynamic-border.js\";\nexport { ExtensionEditorComponent } from \"./extension-editor.js\";\nexport { ExtensionInputComponent } from \"./extension-input.js\";\nexport { ExtensionSelectorComponent } from \"./extension-selector.js\";\nexport { FooterComponent } from \"./footer.js\";\nexport { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from \"./keybinding-hints.js\";\nexport { LoginDialogComponent } from \"./login-dialog.js\";\nexport { ModelSelectorComponent } from \"./model-selector.js\";\nexport { OAuthSelectorComponent } from \"./oauth-selector.js\";\nexport { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from \"./scoped-models-selector.js\";\nexport { SessionSelectorComponent } from \"./session-selector.js\";\nexport { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from \"./settings-selector.js\";\nexport { ShowImagesSelectorComponent } from \"./show-images-selector.js\";\nexport { SkillInvocationMessageComponent } from \"./skill-invocation-message.js\";\nexport { ThemeSelectorComponent } from \"./theme-selector.js\";\nexport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nexport { ToolExecutionComponent, type ToolExecutionOptions } from \"./tool-execution.js\";\nexport { TreeSelectorComponent } from \"./tree-selector.js\";\nexport { UserMessageComponent } from \"./user-message.js\";\nexport { UserMessageSelectorComponent } from \"./user-message-selector.js\";\nexport { truncateToVisualLines, type VisualTruncateResult } from \"./visual-truncate.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/oauth-selector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAA+C,MAAM,sBAAsB,CAAC;AAC9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAIjE;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YACC,IAAI,EAAE,OAAO,GAAG,QAAQ,EACxB,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EACtC,QAAQ,EAAE,MAAM,IAAI,EAgCpB;IAED,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,UAAU;IAoClB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAuBjC;CACD","sourcesContent":["import { getOAuthProviders, type OAuthProviderInfo } from \"@mariozechner/pi-ai\";\nimport { Container, getEditorKeybindings, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport type { AuthStorage } from \"../../../core/auth-storage.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate authStorage: AuthStorage;\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(\n\t\tmode: \"login\" | \"logout\",\n\t\tauthStorage: AuthStorage,\n\t\tonSelect: (providerId: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.authStorage = authStorage;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new TruncatedText(theme.bold(title)));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\t// Check if user is logged in for this provider\n\t\t\tconst credentials = this.authStorage.get(provider.id);\n\t\t\tconst isLoggedIn = credentials?.type === \"oauth\";\n\t\t\tconst statusIndicator = isLoggedIn ? theme.fg(\"success\", \" ✓ logged in\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text + statusIndicator;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text + statusIndicator;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new TruncatedText(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new TruncatedText(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"oauth-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/oauth-selector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAA+C,MAAM,sBAAsB,CAAC;AAC9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAIjE;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YACC,IAAI,EAAE,OAAO,GAAG,QAAQ,EACxB,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EACtC,QAAQ,EAAE,MAAM,IAAI,EAgCpB;IAED,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,UAAU;IAmClB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAuBjC;CACD","sourcesContent":["import { getOAuthProviders, type OAuthProviderInterface } from \"@mariozechner/pi-ai\";\nimport { Container, getEditorKeybindings, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport type { AuthStorage } from \"../../../core/auth-storage.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInterface[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate authStorage: AuthStorage;\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(\n\t\tmode: \"login\" | \"logout\",\n\t\tauthStorage: AuthStorage,\n\t\tonSelect: (providerId: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.authStorage = authStorage;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new TruncatedText(theme.bold(title)));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Check if user is logged in for this provider\n\t\t\tconst credentials = this.authStorage.get(provider.id);\n\t\t\tconst isLoggedIn = credentials?.type === \"oauth\";\n\t\t\tconst statusIndicator = isLoggedIn ? theme.fg(\"success\", \" ✓ logged in\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = theme.fg(\"accent\", provider.name);\n\t\t\t\tline = prefix + text + statusIndicator;\n\t\t\t} else {\n\t\t\t\tconst text = ` ${provider.name}`;\n\t\t\t\tline = text + statusIndicator;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new TruncatedText(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new TruncatedText(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
@@ -47,7 +47,6 @@ export class OAuthSelectorComponent extends Container {
47
47
  if (!provider)
48
48
  continue;
49
49
  const isSelected = i === this.selectedIndex;
50
- const isAvailable = provider.available;
51
50
  // Check if user is logged in for this provider
52
51
  const credentials = this.authStorage.get(provider.id);
53
52
  const isLoggedIn = credentials?.type === "oauth";
@@ -55,11 +54,11 @@ export class OAuthSelectorComponent extends Container {
55
54
  let line = "";
56
55
  if (isSelected) {
57
56
  const prefix = theme.fg("accent", "→ ");
58
- const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
57
+ const text = theme.fg("accent", provider.name);
59
58
  line = prefix + text + statusIndicator;
60
59
  }
61
60
  else {
62
- const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
61
+ const text = ` ${provider.name}`;
63
62
  line = text + statusIndicator;
64
63
  }
65
64
  this.listContainer.addChild(new TruncatedText(line, 0, 0));
@@ -85,7 +84,7 @@ export class OAuthSelectorComponent extends Container {
85
84
  // Enter
86
85
  else if (kb.matches(keyData, "selectConfirm")) {
87
86
  const selectedProvider = this.allProviders[this.selectedIndex];
88
- if (selectedProvider?.available) {
87
+ if (selectedProvider) {
89
88
  this.onSelectCallback(selectedProvider.id);
90
89
  }
91
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/oauth-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { getOAuthProviders, type OAuthProviderInfo } from \"@mariozechner/pi-ai\";\nimport { Container, getEditorKeybindings, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport type { AuthStorage } from \"../../../core/auth-storage.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate authStorage: AuthStorage;\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(\n\t\tmode: \"login\" | \"logout\",\n\t\tauthStorage: AuthStorage,\n\t\tonSelect: (providerId: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.authStorage = authStorage;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new TruncatedText(theme.bold(title)));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\t// Check if user is logged in for this provider\n\t\t\tconst credentials = this.authStorage.get(provider.id);\n\t\t\tconst isLoggedIn = credentials?.type === \"oauth\";\n\t\t\tconst statusIndicator = isLoggedIn ? theme.fg(\"success\", \" ✓ logged in\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text + statusIndicator;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text + statusIndicator;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new TruncatedText(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new TruncatedText(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"oauth-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/oauth-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { getOAuthProviders, type OAuthProviderInterface } from \"@mariozechner/pi-ai\";\nimport { Container, getEditorKeybindings, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport type { AuthStorage } from \"../../../core/auth-storage.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInterface[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate authStorage: AuthStorage;\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(\n\t\tmode: \"login\" | \"logout\",\n\t\tauthStorage: AuthStorage,\n\t\tonSelect: (providerId: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.authStorage = authStorage;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new TruncatedText(theme.bold(title)));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Check if user is logged in for this provider\n\t\t\tconst credentials = this.authStorage.get(provider.id);\n\t\t\tconst isLoggedIn = credentials?.type === \"oauth\";\n\t\t\tconst statusIndicator = isLoggedIn ? theme.fg(\"success\", \" ✓ logged in\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = theme.fg(\"accent\", provider.name);\n\t\t\t\tline = prefix + text + statusIndicator;\n\t\t\t} else {\n\t\t\t\tconst text = ` ${provider.name}`;\n\t\t\t\tline = text + statusIndicator;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new TruncatedText(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new TruncatedText(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
@@ -5,6 +5,7 @@ import { type SortMode } from "./session-selector-search.js";
5
5
  * Custom session list component with multi-line items and search
6
6
  */
7
7
  declare class SessionList implements Component, Focusable {
8
+ getSelectedSessionPath(): string | undefined;
8
9
  private allSessions;
9
10
  private filteredSessions;
10
11
  private selectedIndex;
@@ -22,6 +23,7 @@ declare class SessionList implements Component, Focusable {
22
23
  onTogglePath?: (showPath: boolean) => void;
23
24
  onDeleteConfirmationChange?: (path: string | null) => void;
24
25
  onDeleteSession?: (sessionPath: string) => Promise<void>;
26
+ onRenameSession?: (sessionPath: string) => void;
25
27
  onError?: (message: string) => void;
26
28
  private maxVisible;
27
29
  private _focused;
@@ -42,6 +44,8 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[
42
44
  * Component that renders a session selector
43
45
  */
44
46
  export declare class SessionSelectorComponent extends Container implements Focusable {
47
+ handleInput(data: string): void;
48
+ private canRename;
45
49
  private sessionList;
46
50
  private header;
47
51
  private scope;
@@ -52,15 +56,28 @@ export declare class SessionSelectorComponent extends Container implements Focus
52
56
  private allSessionsLoader;
53
57
  private onCancel;
54
58
  private requestRender;
59
+ private renameSession?;
55
60
  private currentLoading;
56
61
  private allLoading;
57
62
  private allLoadSeq;
63
+ private mode;
64
+ private renameInput;
65
+ private renameTargetPath;
58
66
  private _focused;
59
67
  get focused(): boolean;
60
68
  set focused(value: boolean);
61
- constructor(currentSessionsLoader: SessionsLoader, allSessionsLoader: SessionsLoader, onSelect: (sessionPath: string) => void, onCancel: () => void, onExit: () => void, requestRender: () => void, currentSessionFilePath?: string);
69
+ private buildBaseLayout;
70
+ constructor(currentSessionsLoader: SessionsLoader, allSessionsLoader: SessionsLoader, onSelect: (sessionPath: string) => void, onCancel: () => void, onExit: () => void, requestRender: () => void, options?: {
71
+ renameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;
72
+ showRenameHint?: boolean;
73
+ }, currentSessionFilePath?: string);
62
74
  private loadCurrentSessions;
75
+ private enterRenameMode;
76
+ private exitRenameMode;
77
+ private confirmRename;
78
+ private loadScope;
63
79
  private toggleSortMode;
80
+ private refreshSessionsAfterMutation;
64
81
  private toggleScope;
65
82
  getSessionList(): SessionList;
66
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tgetEditorKeybindings,\n\tInput,\n\tmatchesKey,\n\tSpacer,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport type { SessionInfo, SessionListProgress } from \"../../../core/session-manager.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.js\";\nimport { filterAndSortSessions, type SortMode } from \"./session-selector-search.js\";\n\ntype SessionScope = \"current\" | \"all\";\n\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (!path) return path;\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nfunction formatSessionDate(date: Date): string {\n\tconst now = new Date();\n\tconst diffMs = now.getTime() - date.getTime();\n\tconst diffMins = Math.floor(diffMs / 60000);\n\tconst diffHours = Math.floor(diffMs / 3600000);\n\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\tif (diffMins < 1) return \"just now\";\n\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffDays === 1) return \"1 day ago\";\n\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\treturn date.toLocaleDateString();\n}\n\nclass SessionSelectorHeader implements Component {\n\tprivate scope: SessionScope;\n\tprivate sortMode: SortMode;\n\tprivate requestRender: () => void;\n\tprivate loading = false;\n\tprivate loadProgress: { loaded: number; total: number } | null = null;\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate statusMessage: { type: \"info\" | \"error\"; message: string } | null = null;\n\tprivate statusTimeout: ReturnType<typeof setTimeout> | null = null;\n\n\tconstructor(scope: SessionScope, sortMode: SortMode, requestRender: () => void) {\n\t\tthis.scope = scope;\n\t\tthis.sortMode = sortMode;\n\t\tthis.requestRender = requestRender;\n\t}\n\n\tsetScope(scope: SessionScope): void {\n\t\tthis.scope = scope;\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t}\n\n\tsetLoading(loading: boolean): void {\n\t\tthis.loading = loading;\n\t\t// Progress is scoped to the current load; clear whenever the loading state is set\n\t\tthis.loadProgress = null;\n\t}\n\n\tsetProgress(loaded: number, total: number): void {\n\t\tthis.loadProgress = { loaded, total };\n\t}\n\n\tsetShowPath(showPath: boolean): void {\n\t\tthis.showPath = showPath;\n\t}\n\n\tsetConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t}\n\n\tprivate clearStatusTimeout(): void {\n\t\tif (!this.statusTimeout) return;\n\t\tclearTimeout(this.statusTimeout);\n\t\tthis.statusTimeout = null;\n\t}\n\n\tsetStatusMessage(msg: { type: \"info\" | \"error\"; message: string } | null, autoHideMs?: number): void {\n\t\tthis.clearStatusTimeout();\n\t\tthis.statusMessage = msg;\n\t\tif (!msg || !autoHideMs) return;\n\n\t\tthis.statusTimeout = setTimeout(() => {\n\t\t\tthis.statusMessage = null;\n\t\t\tthis.statusTimeout = null;\n\t\t\tthis.requestRender();\n\t\t}, autoHideMs);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst title = this.scope === \"current\" ? \"Resume Session (Current Folder)\" : \"Resume Session (All)\";\n\t\tconst leftText = theme.bold(title);\n\n\t\tconst sortLabel = this.sortMode === \"recent\" ? \"Recent\" : \"Fuzzy\";\n\t\tconst sortText = theme.fg(\"muted\", \"Sort: \") + theme.fg(\"accent\", sortLabel);\n\n\t\tlet scopeText: string;\n\t\tif (this.loading) {\n\t\t\tconst progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : \"...\";\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", `Loading ${progressText}`)}`;\n\t\t} else if (this.scope === \"current\") {\n\t\t\tscopeText = `${theme.fg(\"accent\", \"◉ Current Folder\")}${theme.fg(\"muted\", \" | ○ All\")}`;\n\t\t} else {\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", \"◉ All\")}`;\n\t\t}\n\n\t\tconst rightText = truncateToWidth(`${scopeText} ${sortText}`, width, \"\");\n\t\tconst availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);\n\t\tconst left = truncateToWidth(leftText, availableLeft, \"\");\n\t\tconst spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));\n\n\t\t// Build hint lines - changes based on state (all branches truncate to width)\n\t\tlet hintLine1: string;\n\t\tlet hintLine2: string;\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tconst confirmHint = \"Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel\";\n\t\t\thintLine1 = theme.fg(\"error\", truncateToWidth(confirmHint, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else if (this.statusMessage) {\n\t\t\tconst color = this.statusMessage.type === \"error\" ? \"error\" : \"accent\";\n\t\t\thintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else {\n\t\t\tconst pathState = this.showPath ? \"(on)\" : \"(off)\";\n\t\t\tconst sep = theme.fg(\"muted\", \" · \");\n\t\t\tconst hint1 = keyHint(\"tab\", \"scope\") + sep + theme.fg(\"muted\", 're:<pattern> regex · \"phrase\" exact');\n\t\t\tconst hint2 =\n\t\t\t\trawKeyHint(\"ctrl+r\", \"sort\") +\n\t\t\t\tsep +\n\t\t\t\trawKeyHint(\"ctrl+d\", \"delete\") +\n\t\t\t\tsep +\n\t\t\t\trawKeyHint(\"ctrl+p\", `path ${pathState}`);\n\t\t\thintLine1 = truncateToWidth(hint1, width, \"…\");\n\t\t\thintLine2 = truncateToWidth(hint2, width, \"…\");\n\t\t}\n\n\t\treturn [`${left}${\" \".repeat(spacing)}${rightText}`, hintLine1, hintLine2];\n\t}\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component, Focusable {\n\tprivate allSessions: SessionInfo[] = [];\n\tprivate filteredSessions: SessionInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tprivate showCwd = false;\n\tprivate sortMode: SortMode = \"relevance\";\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate currentSessionFilePath?: string;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tpublic onExit: () => void = () => {};\n\tpublic onToggleScope?: () => void;\n\tpublic onToggleSort?: () => void;\n\tpublic onTogglePath?: (showPath: boolean) => void;\n\tpublic onDeleteConfirmationChange?: (path: string | null) => void;\n\tpublic onDeleteSession?: (sessionPath: string) => Promise<void>;\n\tpublic onError?: (message: string) => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session: message + metadata + optional path + blank)\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\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\tthis.searchInput.focused = value;\n\t}\n\n\tconstructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\n\t\tthis.showCwd = showCwd;\n\t\tthis.sortMode = sortMode;\n\t\tthis.currentSessionFilePath = currentSessionFilePath;\n\n\t\t// Handle Enter in search input - select current item\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\tif (this.filteredSessions[this.selectedIndex]) {\n\t\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\t\tif (this.onSelect) {\n\t\t\t\t\tthis.onSelect(selected.path);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tsetSessions(sessions: SessionInfo[], showCwd: boolean): void {\n\t\tthis.allSessions = sessions;\n\t\tthis.showCwd = showCwd;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = filterAndSortSessions(this.allSessions, query, this.sortMode);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tprivate setConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t\tthis.onDeleteConfirmationChange?.(path);\n\t}\n\n\tprivate startDeleteConfirmationForSelectedSession(): void {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\tif (!selected) return;\n\n\t\t// Prevent deleting current session\n\t\tif (this.currentSessionFilePath && selected.path === this.currentSessionFilePath) {\n\t\t\tthis.onError?.(\"Cannot delete the currently active session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setConfirmingDeletePath(selected.path);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Render search input\n\t\tlines.push(...this.searchInput.render(width));\n\t\tlines.push(\"\"); // Blank line after search\n\n\t\tif (this.filteredSessions.length === 0) {\n\t\t\tif (this.showCwd) {\n\t\t\t\t// \"All\" scope - no sessions anywhere that match filter\n\t\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\" No sessions found\", width, \"…\")));\n\t\t\t} else {\n\t\t\t\t// \"Current folder\" scope - hint to try \"all\"\n\t\t\t\tlines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"muted\",\n\t\t\t\t\t\ttruncateToWidth(\" No sessions in current folder. Press Tab to view all.\", width, \"…\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);\n\n\t\t// Render visible sessions (message + metadata + optional path + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst session = this.filteredSessions[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isConfirmingDelete = session.path === this.confirmingDeletePath;\n\n\t\t\t// Use session name if set, otherwise first message\n\t\t\tconst hasName = !!session.name;\n\t\t\tconst displayText = session.name ?? session.firstMessage;\n\t\t\tconst normalizedMessage = displayText.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\n\t\t\t// Use warning color for custom names to distinguish from first message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor (2 visible chars)\n\t\t\tconst truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, \"...\");\n\t\t\tlet messageColor: \"error\" | \"warning\" | null = null;\n\t\t\tif (isConfirmingDelete) {\n\t\t\t\tmessageColor = \"error\";\n\t\t\t} else if (hasName) {\n\t\t\t\tmessageColor = \"warning\";\n\t\t\t}\n\t\t\tlet styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;\n\t\t\tif (isSelected) {\n\t\t\t\tstyledMsg = theme.bold(styledMsg);\n\t\t\t}\n\t\t\tconst messageLine = cursor + styledMsg;\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatSessionDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadataParts = [modified, msgCount];\n\t\t\tif (this.showCwd && session.cwd) {\n\t\t\t\tmetadataParts.push(shortenPath(session.cwd));\n\t\t\t}\n\t\t\tconst metadata = ` ${metadataParts.join(\" · \")}`;\n\t\t\tconst truncatedMetadata = truncateToWidth(metadata, width, \"\");\n\t\t\tconst metadataLine = theme.fg(isConfirmingDelete ? \"error\" : \"dim\", truncatedMetadata);\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\n\n\t\t\t// Optional third line: file path (when showPath is enabled)\n\t\t\tif (this.showPath) {\n\t\t\t\tconst pathText = ` ${shortenPath(session.path)}`;\n\t\t\t\tconst truncatedPath = truncateToWidth(pathText, width, \"…\");\n\t\t\t\tconst pathLine = theme.fg(isConfirmingDelete ? \"error\" : \"muted\", truncatedPath);\n\t\t\t\tlines.push(pathLine);\n\t\t\t}\n\n\t\t\tlines.push(\"\"); // Blank line between sessions\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredSessions.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;\n\t\t\tconst scrollInfo = theme.fg(\"muted\", truncateToWidth(scrollText, width, \"\"));\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\n\t\t// Handle delete confirmation state first - intercept all keys\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tif (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\t\tconst pathToDelete = this.confirmingDeletePath;\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\tvoid this.onDeleteSession?.(pathToDelete);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Allow both Escape and Ctrl+C to cancel (consistent with pi UX)\n\t\t\tif (kb.matches(keyData, \"selectCancel\") || matchesKey(keyData, \"ctrl+c\")) {\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Ignore all other keys while confirming\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"tab\")) {\n\t\t\tif (this.onToggleScope) {\n\t\t\t\tthis.onToggleScope();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(keyData, \"ctrl+r\")) {\n\t\t\tthis.onToggleSort?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+P: toggle path display\n\t\tif (matchesKey(keyData, \"ctrl+p\")) {\n\t\t\tthis.showPath = !this.showPath;\n\t\t\tthis.onTogglePath?.(this.showPath);\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)\n\t\tif (matchesKey(keyData, \"ctrl+d\")) {\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+Backspace: non-invasive convenience alias for delete\n\t\t// Only triggers deletion when the query is empty; otherwise it is forwarded to the input\n\t\tif (matchesKey(keyData, \"ctrl+backspace\")) {\n\t\t\tif (this.searchInput.getValue().length > 0) {\n\t\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Page up - jump up by maxVisible items\n\t\telse if (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t}\n\t\t// Page down - jump down by maxVisible items\n\t\telse if (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.path);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t}\n\t}\n}\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/**\n * Delete a session file, trying the `trash` CLI first, then falling back to unlink\n */\nasync function deleteSessionFile(\n\tsessionPath: string,\n): Promise<{ ok: boolean; method: \"trash\" | \"unlink\"; error?: string }> {\n\t// Try `trash` first (if installed)\n\tconst trashArgs = sessionPath.startsWith(\"-\") ? [\"--\", sessionPath] : [sessionPath];\n\tconst trashResult = spawnSync(\"trash\", trashArgs, { encoding: \"utf-8\" });\n\n\tconst getTrashErrorHint = (): string | null => {\n\t\tconst parts: string[] = [];\n\t\tif (trashResult.error) {\n\t\t\tparts.push(trashResult.error.message);\n\t\t}\n\t\tconst stderr = trashResult.stderr?.trim();\n\t\tif (stderr) {\n\t\t\tparts.push(stderr.split(\"\\n\")[0] ?? stderr);\n\t\t}\n\t\tif (parts.length === 0) return null;\n\t\treturn `trash: ${parts.join(\" · \").slice(0, 200)}`;\n\t};\n\n\t// If trash reports success, or the file is gone afterwards, treat it as successful\n\tif (trashResult.status === 0 || !existsSync(sessionPath)) {\n\t\treturn { ok: true, method: \"trash\" };\n\t}\n\n\t// Fallback to permanent deletion\n\ttry {\n\t\tawait unlink(sessionPath);\n\t\treturn { ok: true, method: \"unlink\" };\n\t} catch (err) {\n\t\tconst unlinkError = err instanceof Error ? err.message : String(err);\n\t\tconst trashErrorHint = getTrashErrorHint();\n\t\tconst error = trashErrorHint ? `${unlinkError} (${trashErrorHint})` : unlinkError;\n\t\treturn { ok: false, method: \"unlink\", error };\n\t}\n}\n\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container implements Focusable {\n\tprivate sessionList: SessionList;\n\tprivate header: SessionSelectorHeader;\n\tprivate scope: SessionScope = \"current\";\n\tprivate sortMode: SortMode = \"relevance\";\n\tprivate currentSessions: SessionInfo[] | null = null;\n\tprivate allSessions: SessionInfo[] | null = null;\n\tprivate currentSessionsLoader: SessionsLoader;\n\tprivate allSessionsLoader: SessionsLoader;\n\tprivate onCancel: () => void;\n\tprivate requestRender: () => void;\n\tprivate currentLoading = false;\n\tprivate allLoading = false;\n\tprivate allLoadSeq = 0;\n\n\t// Focusable implementation - propagate to sessionList for IME cursor positioning\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\tthis.sessionList.focused = value;\n\t}\n\n\tconstructor(\n\t\tcurrentSessionsLoader: SessionsLoader,\n\t\tallSessionsLoader: SessionsLoader,\n\t\tonSelect: (sessionPath: string) => void,\n\t\tonCancel: () => void,\n\t\tonExit: () => void,\n\t\trequestRender: () => void,\n\t\tcurrentSessionFilePath?: string,\n\t) {\n\t\tsuper();\n\t\tthis.currentSessionsLoader = currentSessionsLoader;\n\t\tthis.allSessionsLoader = allSessionsLoader;\n\t\tthis.onCancel = onCancel;\n\t\tthis.requestRender = requestRender;\n\t\tthis.header = new SessionSelectorHeader(this.scope, this.sortMode, this.requestRender);\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.header);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list (starts empty, will be populated after load)\n\t\tthis.sessionList = new SessionList([], false, this.sortMode, currentSessionFilePath);\n\n\t\t// Ensure header status timeouts are cleared when leaving the selector\n\t\tconst clearStatusMessage = () => this.header.setStatusMessage(null);\n\t\tthis.sessionList.onSelect = (sessionPath) => {\n\t\t\tclearStatusMessage();\n\t\t\tonSelect(sessionPath);\n\t\t};\n\t\tthis.sessionList.onCancel = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonCancel();\n\t\t};\n\t\tthis.sessionList.onExit = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonExit();\n\t\t};\n\t\tthis.sessionList.onToggleScope = () => this.toggleScope();\n\t\tthis.sessionList.onToggleSort = () => this.toggleSortMode();\n\n\t\t// Sync list events to header\n\t\tthis.sessionList.onTogglePath = (showPath) => {\n\t\t\tthis.header.setShowPath(showPath);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onDeleteConfirmationChange = (path) => {\n\t\t\tthis.header.setConfirmingDeletePath(path);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onError = (msg) => {\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: msg }, 3000);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Handle session deletion\n\t\tthis.sessionList.onDeleteSession = async (sessionPath: string) => {\n\t\t\tconst result = await deleteSessionFile(sessionPath);\n\n\t\t\tif (result.ok) {\n\t\t\t\tif (this.currentSessions) {\n\t\t\t\t\tthis.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\t\t\t\tif (this.allSessions) {\n\t\t\t\t\tthis.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\n\t\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\t\tconst showCwd = this.scope === \"all\";\n\t\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\n\t\t\t\tconst msg = result.method === \"trash\" ? \"Session moved to trash\" : \"Session deleted\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"info\", message: msg }, 2000);\n\t\t\t} else {\n\t\t\t\tconst errorMessage = result.error ?? \"Unknown error\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to delete: ${errorMessage}` }, 3000);\n\t\t\t}\n\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\tthis.addChild(this.sessionList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Start loading current sessions immediately\n\t\tthis.loadCurrentSessions();\n\t}\n\n\tprivate loadCurrentSessions(): void {\n\t\tthis.currentLoading = true;\n\t\tthis.header.setScope(\"current\");\n\t\tthis.header.setLoading(true);\n\t\tthis.requestRender();\n\n\t\tthis.currentSessionsLoader((loaded, total) => {\n\t\t\tif (this.scope !== \"current\") return;\n\t\t\tthis.header.setProgress(loaded, total);\n\t\t\tthis.requestRender();\n\t\t})\n\t\t\t.then((sessions) => {\n\t\t\t\tthis.currentSessions = sessions;\n\t\t\t\tthis.currentLoading = false;\n\n\t\t\t\tif (this.scope !== \"current\") return;\n\n\t\t\t\tthis.header.setLoading(false);\n\t\t\t\tthis.sessionList.setSessions(sessions, false);\n\t\t\t\tthis.requestRender();\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tthis.currentLoading = false;\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\n\t\t\t\tif (this.scope !== \"current\") return;\n\n\t\t\t\tthis.header.setLoading(false);\n\t\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to load sessions: ${message}` }, 4000);\n\t\t\t\tthis.sessionList.setSessions([], false);\n\t\t\t\tthis.requestRender();\n\t\t\t});\n\t}\n\n\tprivate toggleSortMode(): void {\n\t\tthis.sortMode = this.sortMode === \"recent\" ? \"relevance\" : \"recent\";\n\t\tthis.header.setSortMode(this.sortMode);\n\t\tthis.sessionList.setSortMode(this.sortMode);\n\t\tthis.requestRender();\n\t}\n\n\tprivate toggleScope(): void {\n\t\tif (this.scope === \"current\") {\n\t\t\tthis.scope = \"all\";\n\t\t\tthis.header.setScope(this.scope);\n\n\t\t\tif (this.allSessions !== null) {\n\t\t\t\tthis.header.setLoading(false);\n\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.header.setLoading(true);\n\t\t\tthis.sessionList.setSessions([], true);\n\t\t\tthis.requestRender();\n\n\t\t\tif (this.allLoading) return;\n\n\t\t\tthis.allLoading = true;\n\t\t\tconst seq = ++this.allLoadSeq;\n\n\t\t\tthis.allSessionsLoader((loaded, total) => {\n\t\t\t\tif (seq !== this.allLoadSeq) return;\n\t\t\t\tif (this.scope !== \"all\") return;\n\t\t\t\tthis.header.setProgress(loaded, total);\n\t\t\t\tthis.requestRender();\n\t\t\t})\n\t\t\t\t.then((sessions) => {\n\t\t\t\t\tthis.allSessions = sessions;\n\t\t\t\t\tthis.allLoading = false;\n\n\t\t\t\t\tif (seq !== this.allLoadSeq) return;\n\t\t\t\t\tif (this.scope !== \"all\") return;\n\n\t\t\t\t\tthis.header.setLoading(false);\n\t\t\t\t\tthis.sessionList.setSessions(sessions, true);\n\t\t\t\t\tthis.requestRender();\n\n\t\t\t\t\tif (sessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {\n\t\t\t\t\t\tthis.onCancel();\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tthis.allLoading = false;\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\n\t\t\t\t\tif (seq !== this.allLoadSeq) return;\n\t\t\t\t\tif (this.scope !== \"all\") return;\n\n\t\t\t\t\tthis.header.setLoading(false);\n\t\t\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to load sessions: ${message}` }, 4000);\n\t\t\t\t\tthis.sessionList.setSessions([], true);\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t});\n\t\t} else {\n\t\t\tthis.scope = \"current\";\n\t\t\tthis.header.setScope(this.scope);\n\t\t\tthis.header.setLoading(this.currentLoading);\n\t\t\tthis.sessionList.setSessions(this.currentSessions ?? [], false);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tgetEditorKeybindings,\n\tInput,\n\tmatchesKey,\n\tSpacer,\n\tText,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport type { SessionInfo, SessionListProgress } from \"../../../core/session-manager.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\nimport { keyHint } from \"./keybinding-hints.js\";\nimport { filterAndSortSessions, type SortMode } from \"./session-selector-search.js\";\n\ntype SessionScope = \"current\" | \"all\";\n\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (!path) return path;\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nfunction formatSessionDate(date: Date): string {\n\tconst now = new Date();\n\tconst diffMs = now.getTime() - date.getTime();\n\tconst diffMins = Math.floor(diffMs / 60000);\n\tconst diffHours = Math.floor(diffMs / 3600000);\n\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\tif (diffMins < 1) return \"just now\";\n\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffDays === 1) return \"1 day ago\";\n\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\treturn date.toLocaleDateString();\n}\n\nclass SessionSelectorHeader implements Component {\n\tprivate scope: SessionScope;\n\tprivate sortMode: SortMode;\n\tprivate requestRender: () => void;\n\tprivate loading = false;\n\tprivate loadProgress: { loaded: number; total: number } | null = null;\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate statusMessage: { type: \"info\" | \"error\"; message: string } | null = null;\n\tprivate statusTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate showRenameHint = false;\n\n\tconstructor(scope: SessionScope, sortMode: SortMode, requestRender: () => void) {\n\t\tthis.scope = scope;\n\t\tthis.sortMode = sortMode;\n\t\tthis.requestRender = requestRender;\n\t}\n\n\tsetScope(scope: SessionScope): void {\n\t\tthis.scope = scope;\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t}\n\n\tsetLoading(loading: boolean): void {\n\t\tthis.loading = loading;\n\t\t// Progress is scoped to the current load; clear whenever the loading state is set\n\t\tthis.loadProgress = null;\n\t}\n\n\tsetProgress(loaded: number, total: number): void {\n\t\tthis.loadProgress = { loaded, total };\n\t}\n\n\tsetShowPath(showPath: boolean): void {\n\t\tthis.showPath = showPath;\n\t}\n\n\tsetShowRenameHint(show: boolean): void {\n\t\tthis.showRenameHint = show;\n\t}\n\n\tsetConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t}\n\n\tprivate clearStatusTimeout(): void {\n\t\tif (!this.statusTimeout) return;\n\t\tclearTimeout(this.statusTimeout);\n\t\tthis.statusTimeout = null;\n\t}\n\n\tsetStatusMessage(msg: { type: \"info\" | \"error\"; message: string } | null, autoHideMs?: number): void {\n\t\tthis.clearStatusTimeout();\n\t\tthis.statusMessage = msg;\n\t\tif (!msg || !autoHideMs) return;\n\n\t\tthis.statusTimeout = setTimeout(() => {\n\t\t\tthis.statusMessage = null;\n\t\t\tthis.statusTimeout = null;\n\t\t\tthis.requestRender();\n\t\t}, autoHideMs);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst title = this.scope === \"current\" ? \"Resume Session (Current Folder)\" : \"Resume Session (All)\";\n\t\tconst leftText = theme.bold(title);\n\n\t\tconst sortLabel = this.sortMode === \"recent\" ? \"Recent\" : \"Fuzzy\";\n\t\tconst sortText = theme.fg(\"muted\", \"Sort: \") + theme.fg(\"accent\", sortLabel);\n\n\t\tlet scopeText: string;\n\t\tif (this.loading) {\n\t\t\tconst progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : \"...\";\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", `Loading ${progressText}`)}`;\n\t\t} else if (this.scope === \"current\") {\n\t\t\tscopeText = `${theme.fg(\"accent\", \"◉ Current Folder\")}${theme.fg(\"muted\", \" | ○ All\")}`;\n\t\t} else {\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", \"◉ All\")}`;\n\t\t}\n\n\t\tconst rightText = truncateToWidth(`${scopeText} ${sortText}`, width, \"\");\n\t\tconst availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);\n\t\tconst left = truncateToWidth(leftText, availableLeft, \"\");\n\t\tconst spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));\n\n\t\t// Build hint lines - changes based on state (all branches truncate to width)\n\t\tlet hintLine1: string;\n\t\tlet hintLine2: string;\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tconst confirmHint = \"Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel\";\n\t\t\thintLine1 = theme.fg(\"error\", truncateToWidth(confirmHint, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else if (this.statusMessage) {\n\t\t\tconst color = this.statusMessage.type === \"error\" ? \"error\" : \"accent\";\n\t\t\thintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else {\n\t\t\tconst pathState = this.showPath ? \"(on)\" : \"(off)\";\n\t\t\tconst sep = theme.fg(\"muted\", \" · \");\n\t\t\tconst hint1 = keyHint(\"tab\", \"scope\") + sep + theme.fg(\"muted\", 're:<pattern> regex · \"phrase\" exact');\n\t\t\tconst hint2Parts = [\n\t\t\t\tkeyHint(\"toggleSessionSort\", \"sort\"),\n\t\t\t\tkeyHint(\"deleteSession\", \"delete\"),\n\t\t\t\tkeyHint(\"toggleSessionPath\", `path ${pathState}`),\n\t\t\t];\n\t\t\tif (this.showRenameHint) {\n\t\t\t\thint2Parts.push(keyHint(\"renameSession\", \"rename\"));\n\t\t\t}\n\t\t\tconst hint2 = hint2Parts.join(sep);\n\t\t\thintLine1 = truncateToWidth(hint1, width, \"…\");\n\t\t\thintLine2 = truncateToWidth(hint2, width, \"…\");\n\t\t}\n\n\t\treturn [`${left}${\" \".repeat(spacing)}${rightText}`, hintLine1, hintLine2];\n\t}\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component, Focusable {\n\tpublic getSelectedSessionPath(): string | undefined {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\treturn selected?.path;\n\t}\n\tprivate allSessions: SessionInfo[] = [];\n\tprivate filteredSessions: SessionInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tprivate showCwd = false;\n\tprivate sortMode: SortMode = \"relevance\";\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate currentSessionFilePath?: string;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tpublic onExit: () => void = () => {};\n\tpublic onToggleScope?: () => void;\n\tpublic onToggleSort?: () => void;\n\tpublic onTogglePath?: (showPath: boolean) => void;\n\tpublic onDeleteConfirmationChange?: (path: string | null) => void;\n\tpublic onDeleteSession?: (sessionPath: string) => Promise<void>;\n\tpublic onRenameSession?: (sessionPath: string) => void;\n\tpublic onError?: (message: string) => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session: message + metadata + optional path + blank)\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\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\tthis.searchInput.focused = value;\n\t}\n\n\tconstructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\n\t\tthis.showCwd = showCwd;\n\t\tthis.sortMode = sortMode;\n\t\tthis.currentSessionFilePath = currentSessionFilePath;\n\n\t\t// Handle Enter in search input - select current item\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\tif (this.filteredSessions[this.selectedIndex]) {\n\t\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\t\tif (this.onSelect) {\n\t\t\t\t\tthis.onSelect(selected.path);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tsetSessions(sessions: SessionInfo[], showCwd: boolean): void {\n\t\tthis.allSessions = sessions;\n\t\tthis.showCwd = showCwd;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = filterAndSortSessions(this.allSessions, query, this.sortMode);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tprivate setConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t\tthis.onDeleteConfirmationChange?.(path);\n\t}\n\n\tprivate startDeleteConfirmationForSelectedSession(): void {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\tif (!selected) return;\n\n\t\t// Prevent deleting current session\n\t\tif (this.currentSessionFilePath && selected.path === this.currentSessionFilePath) {\n\t\t\tthis.onError?.(\"Cannot delete the currently active session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setConfirmingDeletePath(selected.path);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Render search input\n\t\tlines.push(...this.searchInput.render(width));\n\t\tlines.push(\"\"); // Blank line after search\n\n\t\tif (this.filteredSessions.length === 0) {\n\t\t\tif (this.showCwd) {\n\t\t\t\t// \"All\" scope - no sessions anywhere that match filter\n\t\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(\" No sessions found\", width, \"…\")));\n\t\t\t} else {\n\t\t\t\t// \"Current folder\" scope - hint to try \"all\"\n\t\t\t\tlines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"muted\",\n\t\t\t\t\t\ttruncateToWidth(\" No sessions in current folder. Press Tab to view all.\", width, \"…\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);\n\n\t\t// Render visible sessions (message + metadata + optional path + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst session = this.filteredSessions[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isConfirmingDelete = session.path === this.confirmingDeletePath;\n\n\t\t\t// Use session name if set, otherwise first message\n\t\t\tconst hasName = !!session.name;\n\t\t\tconst displayText = session.name ?? session.firstMessage;\n\t\t\tconst normalizedMessage = displayText.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\n\t\t\t// Use warning color for custom names to distinguish from first message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor (2 visible chars)\n\t\t\tconst truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, \"...\");\n\t\t\tlet messageColor: \"error\" | \"warning\" | null = null;\n\t\t\tif (isConfirmingDelete) {\n\t\t\t\tmessageColor = \"error\";\n\t\t\t} else if (hasName) {\n\t\t\t\tmessageColor = \"warning\";\n\t\t\t}\n\t\t\tlet styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;\n\t\t\tif (isSelected) {\n\t\t\t\tstyledMsg = theme.bold(styledMsg);\n\t\t\t}\n\t\t\tconst messageLine = cursor + styledMsg;\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatSessionDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadataParts = [modified, msgCount];\n\t\t\tif (this.showCwd && session.cwd) {\n\t\t\t\tmetadataParts.push(shortenPath(session.cwd));\n\t\t\t}\n\t\t\tconst metadata = ` ${metadataParts.join(\" · \")}`;\n\t\t\tconst truncatedMetadata = truncateToWidth(metadata, width, \"\");\n\t\t\tconst metadataLine = theme.fg(isConfirmingDelete ? \"error\" : \"dim\", truncatedMetadata);\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\n\n\t\t\t// Optional third line: file path (when showPath is enabled)\n\t\t\tif (this.showPath) {\n\t\t\t\tconst pathText = ` ${shortenPath(session.path)}`;\n\t\t\t\tconst truncatedPath = truncateToWidth(pathText, width, \"…\");\n\t\t\t\tconst pathLine = theme.fg(isConfirmingDelete ? \"error\" : \"muted\", truncatedPath);\n\t\t\t\tlines.push(pathLine);\n\t\t\t}\n\n\t\t\tlines.push(\"\"); // Blank line between sessions\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredSessions.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;\n\t\t\tconst scrollInfo = theme.fg(\"muted\", truncateToWidth(scrollText, width, \"\"));\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\n\t\t// Handle delete confirmation state first - intercept all keys\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tif (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\t\tconst pathToDelete = this.confirmingDeletePath;\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\tvoid this.onDeleteSession?.(pathToDelete);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Allow both Escape and Ctrl+C to cancel (consistent with pi UX)\n\t\t\tif (kb.matches(keyData, \"selectCancel\") || matchesKey(keyData, \"ctrl+c\")) {\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Ignore all other keys while confirming\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"tab\")) {\n\t\t\tif (this.onToggleScope) {\n\t\t\t\tthis.onToggleScope();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"toggleSessionSort\")) {\n\t\t\tthis.onToggleSort?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+P: toggle path display\n\t\tif (kb.matches(keyData, \"toggleSessionPath\")) {\n\t\t\tthis.showPath = !this.showPath;\n\t\t\tthis.onTogglePath?.(this.showPath);\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)\n\t\tif (kb.matches(keyData, \"deleteSession\")) {\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+R: rename selected session\n\t\tif (matchesKey(keyData, \"ctrl+r\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected) {\n\t\t\t\tthis.onRenameSession?.(selected.path);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+Backspace: non-invasive convenience alias for delete\n\t\t// Only triggers deletion when the query is empty; otherwise it is forwarded to the input\n\t\tif (kb.matches(keyData, \"deleteSessionNoninvasive\")) {\n\t\t\tif (this.searchInput.getValue().length > 0) {\n\t\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Page up - jump up by maxVisible items\n\t\telse if (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t}\n\t\t// Page down - jump down by maxVisible items\n\t\telse if (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.path);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t}\n\t}\n}\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/**\n * Delete a session file, trying the `trash` CLI first, then falling back to unlink\n */\nasync function deleteSessionFile(\n\tsessionPath: string,\n): Promise<{ ok: boolean; method: \"trash\" | \"unlink\"; error?: string }> {\n\t// Try `trash` first (if installed)\n\tconst trashArgs = sessionPath.startsWith(\"-\") ? [\"--\", sessionPath] : [sessionPath];\n\tconst trashResult = spawnSync(\"trash\", trashArgs, { encoding: \"utf-8\" });\n\n\tconst getTrashErrorHint = (): string | null => {\n\t\tconst parts: string[] = [];\n\t\tif (trashResult.error) {\n\t\t\tparts.push(trashResult.error.message);\n\t\t}\n\t\tconst stderr = trashResult.stderr?.trim();\n\t\tif (stderr) {\n\t\t\tparts.push(stderr.split(\"\\n\")[0] ?? stderr);\n\t\t}\n\t\tif (parts.length === 0) return null;\n\t\treturn `trash: ${parts.join(\" · \").slice(0, 200)}`;\n\t};\n\n\t// If trash reports success, or the file is gone afterwards, treat it as successful\n\tif (trashResult.status === 0 || !existsSync(sessionPath)) {\n\t\treturn { ok: true, method: \"trash\" };\n\t}\n\n\t// Fallback to permanent deletion\n\ttry {\n\t\tawait unlink(sessionPath);\n\t\treturn { ok: true, method: \"unlink\" };\n\t} catch (err) {\n\t\tconst unlinkError = err instanceof Error ? err.message : String(err);\n\t\tconst trashErrorHint = getTrashErrorHint();\n\t\tconst error = trashErrorHint ? `${unlinkError} (${trashErrorHint})` : unlinkError;\n\t\treturn { ok: false, method: \"unlink\", error };\n\t}\n}\n\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container implements Focusable {\n\thandleInput(data: string): void {\n\t\tif (this.mode === \"rename\") {\n\t\t\tconst kb = getEditorKeybindings();\n\t\t\tif (kb.matches(data, \"selectCancel\") || matchesKey(data, \"ctrl+c\")) {\n\t\t\t\tthis.exitRenameMode();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.renameInput.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.sessionList.handleInput(data);\n\t}\n\n\tprivate canRename = true;\n\tprivate sessionList: SessionList;\n\tprivate header: SessionSelectorHeader;\n\tprivate scope: SessionScope = \"current\";\n\tprivate sortMode: SortMode = \"relevance\";\n\tprivate currentSessions: SessionInfo[] | null = null;\n\tprivate allSessions: SessionInfo[] | null = null;\n\tprivate currentSessionsLoader: SessionsLoader;\n\tprivate allSessionsLoader: SessionsLoader;\n\tprivate onCancel: () => void;\n\tprivate requestRender: () => void;\n\tprivate renameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;\n\tprivate currentLoading = false;\n\tprivate allLoading = false;\n\tprivate allLoadSeq = 0;\n\n\tprivate mode: \"list\" | \"rename\" = \"list\";\n\tprivate renameInput = new Input();\n\tprivate renameTargetPath: string | null = null;\n\n\t// Focusable implementation - propagate to sessionList for IME cursor positioning\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\tthis.sessionList.focused = value;\n\t\tthis.renameInput.focused = value;\n\t\tif (value && this.mode === \"rename\") {\n\t\t\tthis.renameInput.focused = true;\n\t\t}\n\t}\n\n\tprivate buildBaseLayout(content: Component, options?: { showHeader?: boolean }): void {\n\t\tthis.clear();\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder((s) => theme.fg(\"accent\", s)));\n\t\tthis.addChild(new Spacer(1));\n\t\tif (options?.showHeader ?? true) {\n\t\t\tthis.addChild(this.header);\n\t\t\tthis.addChild(new Spacer(1));\n\t\t}\n\t\tthis.addChild(content);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder((s) => theme.fg(\"accent\", s)));\n\t}\n\n\tconstructor(\n\t\tcurrentSessionsLoader: SessionsLoader,\n\t\tallSessionsLoader: SessionsLoader,\n\t\tonSelect: (sessionPath: string) => void,\n\t\tonCancel: () => void,\n\t\tonExit: () => void,\n\t\trequestRender: () => void,\n\t\toptions?: {\n\t\t\trenameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;\n\t\t\tshowRenameHint?: boolean;\n\t\t},\n\t\tcurrentSessionFilePath?: string,\n\t) {\n\t\tsuper();\n\t\tthis.currentSessionsLoader = currentSessionsLoader;\n\t\tthis.allSessionsLoader = allSessionsLoader;\n\t\tthis.onCancel = onCancel;\n\t\tthis.requestRender = requestRender;\n\t\tthis.header = new SessionSelectorHeader(this.scope, this.sortMode, this.requestRender);\n\t\tconst renameSession = options?.renameSession;\n\t\tthis.renameSession = renameSession;\n\t\tthis.canRename = !!renameSession;\n\t\tthis.header.setShowRenameHint(options?.showRenameHint ?? this.canRename);\n\n\t\t// Create session list (starts empty, will be populated after load)\n\t\tthis.sessionList = new SessionList([], false, this.sortMode, currentSessionFilePath);\n\n\t\tthis.buildBaseLayout(this.sessionList);\n\n\t\tthis.renameInput.onSubmit = (value) => {\n\t\t\tvoid this.confirmRename(value);\n\t\t};\n\n\t\t// Ensure header status timeouts are cleared when leaving the selector\n\t\tconst clearStatusMessage = () => this.header.setStatusMessage(null);\n\t\tthis.sessionList.onSelect = (sessionPath) => {\n\t\t\tclearStatusMessage();\n\t\t\tonSelect(sessionPath);\n\t\t};\n\t\tthis.sessionList.onCancel = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonCancel();\n\t\t};\n\t\tthis.sessionList.onExit = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonExit();\n\t\t};\n\t\tthis.sessionList.onToggleScope = () => this.toggleScope();\n\t\tthis.sessionList.onToggleSort = () => this.toggleSortMode();\n\t\tthis.sessionList.onRenameSession = (sessionPath) => {\n\t\t\tif (!renameSession) return;\n\t\t\tif (this.scope === \"current\" && this.currentLoading) return;\n\t\t\tif (this.scope === \"all\" && this.allLoading) return;\n\n\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\tconst session = sessions.find((s) => s.path === sessionPath);\n\t\t\tthis.enterRenameMode(sessionPath, session?.name);\n\t\t};\n\n\t\t// Sync list events to header\n\t\tthis.sessionList.onTogglePath = (showPath) => {\n\t\t\tthis.header.setShowPath(showPath);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onDeleteConfirmationChange = (path) => {\n\t\t\tthis.header.setConfirmingDeletePath(path);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onError = (msg) => {\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: msg }, 3000);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Handle session deletion\n\t\tthis.sessionList.onDeleteSession = async (sessionPath: string) => {\n\t\t\tconst result = await deleteSessionFile(sessionPath);\n\n\t\t\tif (result.ok) {\n\t\t\t\tif (this.currentSessions) {\n\t\t\t\t\tthis.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\t\t\t\tif (this.allSessions) {\n\t\t\t\t\tthis.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\n\t\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\t\tconst showCwd = this.scope === \"all\";\n\t\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\n\t\t\t\tconst msg = result.method === \"trash\" ? \"Session moved to trash\" : \"Session deleted\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"info\", message: msg }, 2000);\n\t\t\t\tawait this.refreshSessionsAfterMutation();\n\t\t\t} else {\n\t\t\t\tconst errorMessage = result.error ?? \"Unknown error\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to delete: ${errorMessage}` }, 3000);\n\t\t\t}\n\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Start loading current sessions immediately\n\t\tthis.loadCurrentSessions();\n\t}\n\n\tprivate loadCurrentSessions(): void {\n\t\tvoid this.loadScope(\"current\", \"initial\");\n\t}\n\n\tprivate enterRenameMode(sessionPath: string, currentName: string | undefined): void {\n\t\tthis.mode = \"rename\";\n\t\tthis.renameTargetPath = sessionPath;\n\t\tthis.renameInput.setValue(currentName ?? \"\");\n\t\tthis.renameInput.focused = true;\n\n\t\tconst panel = new Container();\n\t\tpanel.addChild(new Text(theme.bold(\"Rename Session\"), 1, 0));\n\t\tpanel.addChild(new Spacer(1));\n\t\tpanel.addChild(this.renameInput);\n\t\tpanel.addChild(new Spacer(1));\n\t\tpanel.addChild(new Text(theme.fg(\"muted\", \"Enter to save · Esc/Ctrl+C to cancel\"), 1, 0));\n\n\t\tthis.buildBaseLayout(panel, { showHeader: false });\n\t\tthis.requestRender();\n\t}\n\n\tprivate exitRenameMode(): void {\n\t\tthis.mode = \"list\";\n\t\tthis.renameTargetPath = null;\n\n\t\tthis.buildBaseLayout(this.sessionList);\n\n\t\tthis.requestRender();\n\t}\n\n\tprivate async confirmRename(value: string): Promise<void> {\n\t\tconst next = value.trim();\n\t\tif (!next) return;\n\t\tconst target = this.renameTargetPath;\n\t\tif (!target) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\n\t\t// Find current name for callback\n\t\tconst renameSession = this.renameSession;\n\t\tif (!renameSession) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait renameSession(target, next);\n\t\t\tawait this.refreshSessionsAfterMutation();\n\t\t} finally {\n\t\t\tthis.exitRenameMode();\n\t\t}\n\t}\n\n\tprivate async loadScope(scope: SessionScope, reason: \"initial\" | \"refresh\" | \"toggle\"): Promise<void> {\n\t\tconst showCwd = scope === \"all\";\n\n\t\t// Mark loading\n\t\tif (scope === \"current\") {\n\t\t\tthis.currentLoading = true;\n\t\t} else {\n\t\t\tthis.allLoading = true;\n\t\t}\n\n\t\tconst seq = scope === \"all\" ? ++this.allLoadSeq : undefined;\n\t\tthis.header.setScope(scope);\n\t\tthis.header.setLoading(true);\n\t\tthis.requestRender();\n\n\t\tconst onProgress = (loaded: number, total: number) => {\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\t\t\tthis.header.setProgress(loaded, total);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\ttry {\n\t\t\tconst sessions = await (scope === \"current\"\n\t\t\t\t? this.currentSessionsLoader(onProgress)\n\t\t\t\t: this.allSessionsLoader(onProgress));\n\n\t\t\tif (scope === \"current\") {\n\t\t\t\tthis.currentSessions = sessions;\n\t\t\t\tthis.currentLoading = false;\n\t\t\t} else {\n\t\t\t\tthis.allSessions = sessions;\n\t\t\t\tthis.allLoading = false;\n\t\t\t}\n\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\t\t\tthis.requestRender();\n\n\t\t\tif (scope === \"all\" && sessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (scope === \"current\") {\n\t\t\t\tthis.currentLoading = false;\n\t\t\t} else {\n\t\t\t\tthis.allLoading = false;\n\t\t\t}\n\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to load sessions: ${message}` }, 4000);\n\n\t\t\tif (reason === \"initial\") {\n\t\t\t\tthis.sessionList.setSessions([], showCwd);\n\t\t\t}\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate toggleSortMode(): void {\n\t\tthis.sortMode = this.sortMode === \"recent\" ? \"relevance\" : \"recent\";\n\t\tthis.header.setSortMode(this.sortMode);\n\t\tthis.sessionList.setSortMode(this.sortMode);\n\t\tthis.requestRender();\n\t}\n\n\tprivate async refreshSessionsAfterMutation(): Promise<void> {\n\t\tawait this.loadScope(this.scope, \"refresh\");\n\t}\n\n\tprivate toggleScope(): void {\n\t\tif (this.scope === \"current\") {\n\t\t\tthis.scope = \"all\";\n\t\t\tthis.header.setScope(this.scope);\n\n\t\t\tif (this.allSessions !== null) {\n\t\t\t\tthis.header.setLoading(false);\n\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!this.allLoading) {\n\t\t\t\tvoid this.loadScope(\"all\", \"toggle\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scope = \"current\";\n\t\tthis.header.setScope(this.scope);\n\t\tthis.header.setLoading(this.currentLoading);\n\t\tthis.sessionList.setSessions(this.currentSessions ?? [], false);\n\t\tthis.requestRender();\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}