@mariozechner/pi-coding-agent 0.23.3 → 0.23.5

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 (120) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +20 -13
  3. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  4. package/dist/core/custom-tools/loader.js +56 -4
  5. package/dist/core/custom-tools/loader.js.map +1 -1
  6. package/dist/core/custom-tools/types.d.ts +9 -1
  7. package/dist/core/custom-tools/types.d.ts.map +1 -1
  8. package/dist/core/custom-tools/types.js.map +1 -1
  9. package/dist/core/hooks/index.d.ts +2 -1
  10. package/dist/core/hooks/index.d.ts.map +1 -1
  11. package/dist/core/hooks/index.js +1 -0
  12. package/dist/core/hooks/index.js.map +1 -1
  13. package/dist/core/hooks/loader.d.ts.map +1 -1
  14. package/dist/core/hooks/loader.js +4 -2
  15. package/dist/core/hooks/loader.js.map +1 -1
  16. package/dist/core/hooks/runner.d.ts.map +1 -1
  17. package/dist/core/hooks/runner.js +44 -4
  18. package/dist/core/hooks/runner.js.map +1 -1
  19. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  20. package/dist/core/hooks/tool-wrapper.js +5 -9
  21. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  22. package/dist/core/hooks/types.d.ts +73 -11
  23. package/dist/core/hooks/types.d.ts.map +1 -1
  24. package/dist/core/hooks/types.js +22 -1
  25. package/dist/core/hooks/types.js.map +1 -1
  26. package/dist/core/skills.d.ts +18 -5
  27. package/dist/core/skills.d.ts.map +1 -1
  28. package/dist/core/skills.js +183 -72
  29. package/dist/core/skills.js.map +1 -1
  30. package/dist/core/slash-commands.d.ts.map +1 -1
  31. package/dist/core/slash-commands.js +2 -2
  32. package/dist/core/slash-commands.js.map +1 -1
  33. package/dist/core/system-prompt.d.ts.map +1 -1
  34. package/dist/core/system-prompt.js +2 -2
  35. package/dist/core/system-prompt.js.map +1 -1
  36. package/dist/core/tools/bash.d.ts +5 -0
  37. package/dist/core/tools/bash.d.ts.map +1 -1
  38. package/dist/core/tools/bash.js.map +1 -1
  39. package/dist/core/tools/find.d.ts +5 -0
  40. package/dist/core/tools/find.d.ts.map +1 -1
  41. package/dist/core/tools/find.js.map +1 -1
  42. package/dist/core/tools/grep.d.ts +6 -0
  43. package/dist/core/tools/grep.d.ts.map +1 -1
  44. package/dist/core/tools/grep.js.map +1 -1
  45. package/dist/core/tools/index.d.ts +6 -5
  46. package/dist/core/tools/index.d.ts.map +1 -1
  47. package/dist/core/tools/index.js.map +1 -1
  48. package/dist/core/tools/ls.d.ts +5 -0
  49. package/dist/core/tools/ls.d.ts.map +1 -1
  50. package/dist/core/tools/ls.js.map +1 -1
  51. package/dist/core/tools/read.d.ts +4 -0
  52. package/dist/core/tools/read.d.ts.map +1 -1
  53. package/dist/core/tools/read.js.map +1 -1
  54. package/dist/index.d.ts +5 -3
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +5 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/main.d.ts.map +1 -1
  59. package/dist/main.js +4 -0
  60. package/dist/main.js.map +1 -1
  61. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  62. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/custom-editor.js +16 -7
  64. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  65. package/dist/modes/interactive/components/diff.d.ts +12 -0
  66. package/dist/modes/interactive/components/diff.d.ts.map +1 -0
  67. package/dist/modes/interactive/components/diff.js +133 -0
  68. package/dist/modes/interactive/components/diff.js.map +1 -0
  69. package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
  70. package/dist/modes/interactive/components/hook-input.js +2 -2
  71. package/dist/modes/interactive/components/hook-input.js.map +1 -1
  72. package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
  73. package/dist/modes/interactive/components/hook-selector.js +2 -2
  74. package/dist/modes/interactive/components/hook-selector.js.map +1 -1
  75. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/model-selector.js +2 -2
  77. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  78. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  79. package/dist/modes/interactive/components/oauth-selector.js +2 -2
  80. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  81. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/session-selector.js +3 -3
  83. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  84. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/dist/modes/interactive/components/tool-execution.js +26 -20
  86. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  88. package/dist/modes/interactive/components/user-message-selector.js +3 -3
  89. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  90. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  91. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  92. package/dist/modes/interactive/interactive-mode.js +63 -1
  93. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  94. package/dist/modes/interactive/theme/dark.json +9 -9
  95. package/dist/modes/interactive/theme/light.json +9 -9
  96. package/dist/modes/interactive/theme/theme.d.ts +10 -0
  97. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  98. package/dist/modes/interactive/theme/theme.js +131 -3
  99. package/dist/modes/interactive/theme/theme.js.map +1 -1
  100. package/dist/modes/print-mode.d.ts.map +1 -1
  101. package/dist/modes/print-mode.js +10 -0
  102. package/dist/modes/print-mode.js.map +1 -1
  103. package/docs/custom-tools.md +43 -4
  104. package/docs/hooks.md +104 -5
  105. package/docs/skills.md +65 -24
  106. package/examples/custom-tools/README.md +18 -7
  107. package/examples/custom-tools/subagent/README.md +172 -0
  108. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  109. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  110. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  111. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  112. package/examples/custom-tools/subagent/agents.ts +157 -0
  113. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  114. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  115. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  116. package/examples/custom-tools/subagent/index.ts +772 -0
  117. package/package.json +6 -6
  118. /package/examples/custom-tools/{hello.ts → hello/index.ts} +0 -0
  119. /package/examples/custom-tools/{question.ts → question/index.ts} +0 -0
  120. /package/examples/custom-tools/{todo.ts → todo/index.ts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAgB,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,GAAG,CAAM;IAEjB,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,eAAe,EAAE,eAAe,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EA+CpB;YAEa,UAAU;IAgCxB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,UAAU;IAqDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA2BjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../../../core/model-config.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\n/**\n * Component that renders a model selector with search\n */\nexport class ModelSelectorComponent extends Container {\n\tprivate searchInput: Input;\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel: Model<any> | null;\n\tprivate settingsManager: SettingsManager;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\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 hint about API key filtering\n\t\tthis.addChild(\n\t\t\tnew Text(theme.fg(\"warning\", \"Only showing models with configured API keys (see README for details)\"), 0, 0),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\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// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tthis.updateList();\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\t// Load available models fresh (includes custom models from models.json)\n\t\tconst { models: availableModels, error } = await getAvailableModels();\n\n\t\t// If there's an error loading models.json, we'll show it via the \"no models\" path\n\t\t// The error will be displayed to the user\n\t\tif (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error;\n\t\t\treturn;\n\t\t}\n\n\t\tconst models: ModelItem[] = availableModels.map((model) => ({\n\t\t\tprovider: model.provider,\n\t\t\tid: model.id,\n\t\t\tmodel,\n\t\t}));\n\n\t\t// Sort: current model first, then by provider\n\t\tmodels.sort((a, b) => {\n\t\t\tconst aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;\n\t\t\tconst bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = this.currentModel?.id === item.model.id;\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 modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = prefix + theme.fg(\"accent\", modelText) + \" \" + providerBadge + checkmark;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = modelText + \" \" + providerBadge + checkmark;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\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.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,KAAK,EAA0B,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAE1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,GAAG,CAAM;IAEjB,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,eAAe,EAAE,eAAe,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EA+CpB;YAEa,UAAU;IAgCxB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,UAAU;IAqDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA2BjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, isEscape, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../../../core/model-config.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\n/**\n * Component that renders a model selector with search\n */\nexport class ModelSelectorComponent extends Container {\n\tprivate searchInput: Input;\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel: Model<any> | null;\n\tprivate settingsManager: SettingsManager;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\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 hint about API key filtering\n\t\tthis.addChild(\n\t\t\tnew Text(theme.fg(\"warning\", \"Only showing models with configured API keys (see README for details)\"), 0, 0),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\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// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tthis.updateList();\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\t// Load available models fresh (includes custom models from models.json)\n\t\tconst { models: availableModels, error } = await getAvailableModels();\n\n\t\t// If there's an error loading models.json, we'll show it via the \"no models\" path\n\t\t// The error will be displayed to the user\n\t\tif (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error;\n\t\t\treturn;\n\t\t}\n\n\t\tconst models: ModelItem[] = availableModels.map((model) => ({\n\t\t\tprovider: model.provider,\n\t\t\tid: model.id,\n\t\t\tmodel,\n\t\t}));\n\n\t\t// Sort: current model first, then by provider\n\t\tmodels.sort((a, b) => {\n\t\t\tconst aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;\n\t\t\tconst bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = this.currentModel?.id === item.model.id;\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 modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = prefix + theme.fg(\"accent\", modelText) + \" \" + providerBadge + checkmark;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = modelText + \" \" + providerBadge + checkmark;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\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.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Container, Input, Spacer, Text } from "@mariozechner/pi-tui";
1
+ import { Container, Input, isEscape, Spacer, Text } from "@mariozechner/pi-tui";
2
2
  import { getAvailableModels } from "../../../core/model-config.js";
3
3
  import { fuzzyFilter } from "../../../utils/fuzzy.js";
4
4
  import { theme } from "../theme/theme.js";
@@ -152,7 +152,7 @@ export class ModelSelectorComponent extends Container {
152
152
  }
153
153
  }
154
154
  // Escape
155
- else if (keyData === "\x1b") {
155
+ else if (isEscape(keyData)) {
156
156
  this.onCancelCallback();
157
157
  }
158
158
  // Pass everything else to search input
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../../../core/model-config.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\n/**\n * Component that renders a model selector with search\n */\nexport class ModelSelectorComponent extends Container {\n\tprivate searchInput: Input;\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel: Model<any> | null;\n\tprivate settingsManager: SettingsManager;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\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 hint about API key filtering\n\t\tthis.addChild(\n\t\t\tnew Text(theme.fg(\"warning\", \"Only showing models with configured API keys (see README for details)\"), 0, 0),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\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// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tthis.updateList();\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\t// Load available models fresh (includes custom models from models.json)\n\t\tconst { models: availableModels, error } = await getAvailableModels();\n\n\t\t// If there's an error loading models.json, we'll show it via the \"no models\" path\n\t\t// The error will be displayed to the user\n\t\tif (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error;\n\t\t\treturn;\n\t\t}\n\n\t\tconst models: ModelItem[] = availableModels.map((model) => ({\n\t\t\tprovider: model.provider,\n\t\t\tid: model.id,\n\t\t\tmodel,\n\t\t}));\n\n\t\t// Sort: current model first, then by provider\n\t\tmodels.sort((a, b) => {\n\t\t\tconst aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;\n\t\t\tconst bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = this.currentModel?.id === item.model.id;\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 modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = prefix + theme.fg(\"accent\", modelText) + \" \" + providerBadge + checkmark;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = modelText + \" \" + providerBadge + checkmark;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\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.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, isEscape, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../../../core/model-config.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\n/**\n * Component that renders a model selector with search\n */\nexport class ModelSelectorComponent extends Container {\n\tprivate searchInput: Input;\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel: Model<any> | null;\n\tprivate settingsManager: SettingsManager;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\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 hint about API key filtering\n\t\tthis.addChild(\n\t\t\tnew Text(theme.fg(\"warning\", \"Only showing models with configured API keys (see README for details)\"), 0, 0),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\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// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tthis.updateList();\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\t// Load available models fresh (includes custom models from models.json)\n\t\tconst { models: availableModels, error } = await getAvailableModels();\n\n\t\t// If there's an error loading models.json, we'll show it via the \"no models\" path\n\t\t// The error will be displayed to the user\n\t\tif (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error;\n\t\t\treturn;\n\t\t}\n\n\t\tconst models: ModelItem[] = availableModels.map((model) => ({\n\t\t\tprovider: model.provider,\n\t\t\tid: model.id,\n\t\t\tmodel,\n\t\t}));\n\n\t\t// Sort: current model first, then by provider\n\t\tmodels.sort((a, b) => {\n\t\t\tconst aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;\n\t\t\tconst bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = this.currentModel?.id === item.model.id;\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 modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = prefix + theme.fg(\"accent\", modelText) + \" \" + providerBadge + checkmark;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = modelText + \" \" + providerBadge + checkmark;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\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.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/oauth-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyB,MAAM,sBAAsB,CAAC;AAMxE;;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,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YAAY,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA8BjG;IAED,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,UAAU;IAoClB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAsBjC;CACD","sourcesContent":["import { Container, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../../../core/oauth/index.js\";\nimport { loadOAuthCredentials } from \"../../../core/oauth/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 onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\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\tthis.allProviders = this.allProviders.filter((p) => p.available);\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 = loadOAuthCredentials(provider.id);\n\t\t\tconst isLoggedIn = credentials !== null;\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\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 (keyData === \"\\x1b[B\") {\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 (keyData === \"\\r\") {\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\n\t\telse if (keyData === \"\\x1b\") {\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":"AAAA,OAAO,EAAE,SAAS,EAAmC,MAAM,sBAAsB,CAAC;AAMlF;;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,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YAAY,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA8BjG;IAED,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,UAAU;IAoClB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAsBjC;CACD","sourcesContent":["import { Container, isEscape, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../../../core/oauth/index.js\";\nimport { loadOAuthCredentials } from \"../../../core/oauth/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 onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\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\tthis.allProviders = this.allProviders.filter((p) => p.available);\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 = loadOAuthCredentials(provider.id);\n\t\t\tconst isLoggedIn = credentials !== null;\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\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 (keyData === \"\\x1b[B\") {\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 (keyData === \"\\r\") {\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\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Container, Spacer, TruncatedText } from "@mariozechner/pi-tui";
1
+ import { Container, isEscape, Spacer, TruncatedText } from "@mariozechner/pi-tui";
2
2
  import { getOAuthProviders } from "../../../core/oauth/index.js";
3
3
  import { loadOAuthCredentials } from "../../../core/oauth/storage.js";
4
4
  import { theme } from "../theme/theme.js";
@@ -89,7 +89,7 @@ export class OAuthSelectorComponent extends Container {
89
89
  }
90
90
  }
91
91
  // Escape
92
- else if (keyData === "\x1b") {
92
+ else if (isEscape(keyData)) {
93
93
  this.onCancelCallback();
94
94
  }
95
95
  }
@@ -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 { Container, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../../../core/oauth/index.js\";\nimport { loadOAuthCredentials } from \"../../../core/oauth/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 onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\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\tthis.allProviders = this.allProviders.filter((p) => p.available);\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 = loadOAuthCredentials(provider.id);\n\t\t\tconst isLoggedIn = credentials !== null;\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\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 (keyData === \"\\x1b[B\") {\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 (keyData === \"\\r\") {\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\n\t\telse if (keyData === \"\\x1b\") {\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 { Container, isEscape, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../../../core/oauth/index.js\";\nimport { loadOAuthCredentials } from \"../../../core/oauth/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 onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\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\tthis.allProviders = this.allProviders.filter((p) => p.available);\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 = loadOAuthCredentials(provider.id);\n\t\t\tconst isLoggedIn = credentials !== null;\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\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 (keyData === \"\\x1b[B\") {\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 (keyData === \"\\r\") {\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\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAwC,MAAM,sBAAsB,CAAC;AACvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAKvE,UAAU,WAAW;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,cAAM,WAAY,YAAW,SAAS;IACrC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,WAAW,CAAQ;IACpB,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,OAAO,CAAC,UAAU,CAAa;IAE/B,YAAY,QAAQ,EAAE,WAAW,EAAE,EAclC;IAED,OAAO,CAAC,cAAc;IAKtB,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqE9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA+BjC;CACD;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,SAAS;IACtD,OAAO,CAAC,WAAW,CAAc;IAEjC,YAAY,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA4BxG;IAED,cAAc,IAAI,WAAW,CAE5B;CACD","sourcesContent":["import { type Component, Container, Input, Spacer, Text, truncateToWidth } from \"@mariozechner/pi-tui\";\nimport type { SessionManager } from \"../../../core/session-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface SessionItem {\n\tpath: string;\n\tid: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n\tallMessagesText: string;\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component {\n\tprivate allSessions: SessionItem[] = [];\n\tprivate filteredSessions: SessionItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)\n\n\tconstructor(sessions: SessionItem[]) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\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\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => session.allMessagesText);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\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\tlines.push(theme.fg(\"muted\", \" No sessions found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Format dates\n\t\tconst formatDate = (date: Date): string => {\n\t\t\tconst now = new Date();\n\t\t\tconst diffMs = now.getTime() - date.getTime();\n\t\t\tconst diffMins = Math.floor(diffMs / 60000);\n\t\t\tconst diffHours = Math.floor(diffMs / 3600000);\n\t\t\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\t\t\tif (diffMins < 1) return \"just now\";\n\t\t\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffDays === 1) return \"1 day ago\";\n\t\t\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\t\t\treturn date.toLocaleDateString();\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 (2 lines per session + 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\n\t\t\t// Normalize first message to single line\n\t\t\tconst normalizedMessage = session.firstMessage.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\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\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadata = ` ${modified} · ${msgCount}`;\n\t\t\tconst metadataLine = theme.fg(\"dim\", truncateToWidth(metadata, width, \"\"));\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\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 (keyData === \"\\x1b\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - exit process\n\t\telse if (keyData === \"\\x03\") {\n\t\t\tprocess.exit(0);\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\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container {\n\tprivate sessionList: SessionList;\n\n\tconstructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Load all sessions\n\t\tconst sessions = sessionManager.loadAllSessions();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Resume Session\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list\n\t\tthis.sessionList = new SessionList(sessions);\n\t\tthis.sessionList.onSelect = onSelect;\n\t\tthis.sessionList.onCancel = onCancel;\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// Auto-cancel if no sessions\n\t\tif (sessions.length === 0) {\n\t\t\tsetTimeout(() => onCancel(), 100);\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":"AAAA,OAAO,EACN,KAAK,SAAS,EACd,SAAS,EAOT,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAKvE,UAAU,WAAW;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,cAAM,WAAY,YAAW,SAAS;IACrC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,WAAW,CAAQ;IACpB,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,OAAO,CAAC,UAAU,CAAa;IAE/B,YAAY,QAAQ,EAAE,WAAW,EAAE,EAclC;IAED,OAAO,CAAC,cAAc;IAKtB,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqE9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA+BjC;CACD;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,SAAS;IACtD,OAAO,CAAC,WAAW,CAAc;IAEjC,YAAY,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA4BxG;IAED,cAAc,IAAI,WAAW,CAE5B;CACD","sourcesContent":["import {\n\ttype Component,\n\tContainer,\n\tInput,\n\tisCtrlC,\n\tisEscape,\n\tSpacer,\n\tText,\n\ttruncateToWidth,\n} from \"@mariozechner/pi-tui\";\nimport type { SessionManager } from \"../../../core/session-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface SessionItem {\n\tpath: string;\n\tid: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n\tallMessagesText: string;\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component {\n\tprivate allSessions: SessionItem[] = [];\n\tprivate filteredSessions: SessionItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)\n\n\tconstructor(sessions: SessionItem[]) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\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\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => session.allMessagesText);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\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\tlines.push(theme.fg(\"muted\", \" No sessions found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Format dates\n\t\tconst formatDate = (date: Date): string => {\n\t\t\tconst now = new Date();\n\t\t\tconst diffMs = now.getTime() - date.getTime();\n\t\t\tconst diffMins = Math.floor(diffMs / 60000);\n\t\t\tconst diffHours = Math.floor(diffMs / 3600000);\n\t\t\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\t\t\tif (diffMins < 1) return \"just now\";\n\t\t\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffDays === 1) return \"1 day ago\";\n\t\t\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\t\t\treturn date.toLocaleDateString();\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 (2 lines per session + 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\n\t\t\t// Normalize first message to single line\n\t\t\tconst normalizedMessage = session.firstMessage.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\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\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadata = ` ${modified} · ${msgCount}`;\n\t\t\tconst metadataLine = theme.fg(\"dim\", truncateToWidth(metadata, width, \"\"));\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\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 (isEscape(keyData)) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - exit process\n\t\telse if (isCtrlC(keyData)) {\n\t\t\tprocess.exit(0);\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\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container {\n\tprivate sessionList: SessionList;\n\n\tconstructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Load all sessions\n\t\tconst sessions = sessionManager.loadAllSessions();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Resume Session\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list\n\t\tthis.sessionList = new SessionList(sessions);\n\t\tthis.sessionList.onSelect = onSelect;\n\t\tthis.sessionList.onCancel = onCancel;\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// Auto-cancel if no sessions\n\t\tif (sessions.length === 0) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Container, Input, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
1
+ import { Container, Input, isCtrlC, isEscape, Spacer, Text, truncateToWidth, } from "@mariozechner/pi-tui";
2
2
  import { fuzzyFilter } from "../../../utils/fuzzy.js";
3
3
  import { theme } from "../theme/theme.js";
4
4
  import { DynamicBorder } from "./dynamic-border.js";
@@ -110,13 +110,13 @@ class SessionList {
110
110
  }
111
111
  }
112
112
  // Escape - cancel
113
- else if (keyData === "\x1b") {
113
+ else if (isEscape(keyData)) {
114
114
  if (this.onCancel) {
115
115
  this.onCancel();
116
116
  }
117
117
  }
118
118
  // Ctrl+C - exit process
119
- else if (keyData === "\x03") {
119
+ else if (isCtrlC(keyData)) {
120
120
  process.exit(0);
121
121
  }
122
122
  // Pass everything else to search input
@@ -1 +1 @@
1
- {"version":3,"file":"session-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, Container, Input, Spacer, Text, truncateToWidth } from \"@mariozechner/pi-tui\";\nimport type { SessionManager } from \"../../../core/session-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface SessionItem {\n\tpath: string;\n\tid: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n\tallMessagesText: string;\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component {\n\tprivate allSessions: SessionItem[] = [];\n\tprivate filteredSessions: SessionItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)\n\n\tconstructor(sessions: SessionItem[]) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\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\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => session.allMessagesText);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\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\tlines.push(theme.fg(\"muted\", \" No sessions found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Format dates\n\t\tconst formatDate = (date: Date): string => {\n\t\t\tconst now = new Date();\n\t\t\tconst diffMs = now.getTime() - date.getTime();\n\t\t\tconst diffMins = Math.floor(diffMs / 60000);\n\t\t\tconst diffHours = Math.floor(diffMs / 3600000);\n\t\t\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\t\t\tif (diffMins < 1) return \"just now\";\n\t\t\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffDays === 1) return \"1 day ago\";\n\t\t\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\t\t\treturn date.toLocaleDateString();\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 (2 lines per session + 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\n\t\t\t// Normalize first message to single line\n\t\t\tconst normalizedMessage = session.firstMessage.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\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\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadata = ` ${modified} · ${msgCount}`;\n\t\t\tconst metadataLine = theme.fg(\"dim\", truncateToWidth(metadata, width, \"\"));\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\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 (keyData === \"\\x1b\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - exit process\n\t\telse if (keyData === \"\\x03\") {\n\t\t\tprocess.exit(0);\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\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container {\n\tprivate sessionList: SessionList;\n\n\tconstructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Load all sessions\n\t\tconst sessions = sessionManager.loadAllSessions();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Resume Session\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list\n\t\tthis.sessionList = new SessionList(sessions);\n\t\tthis.sessionList.onSelect = onSelect;\n\t\tthis.sessionList.onCancel = onCancel;\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// Auto-cancel if no sessions\n\t\tif (sessions.length === 0) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Component,\n\tContainer,\n\tInput,\n\tisCtrlC,\n\tisEscape,\n\tSpacer,\n\tText,\n\ttruncateToWidth,\n} from \"@mariozechner/pi-tui\";\nimport type { SessionManager } from \"../../../core/session-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface SessionItem {\n\tpath: string;\n\tid: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n\tallMessagesText: string;\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component {\n\tprivate allSessions: SessionItem[] = [];\n\tprivate filteredSessions: SessionItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)\n\n\tconstructor(sessions: SessionItem[]) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\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\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => session.allMessagesText);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\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\tlines.push(theme.fg(\"muted\", \" No sessions found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Format dates\n\t\tconst formatDate = (date: Date): string => {\n\t\t\tconst now = new Date();\n\t\t\tconst diffMs = now.getTime() - date.getTime();\n\t\t\tconst diffMins = Math.floor(diffMs / 60000);\n\t\t\tconst diffHours = Math.floor(diffMs / 3600000);\n\t\t\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\t\t\tif (diffMins < 1) return \"just now\";\n\t\t\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\t\t\tif (diffDays === 1) return \"1 day ago\";\n\t\t\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\t\t\treturn date.toLocaleDateString();\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 (2 lines per session + 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\n\t\t\t// Normalize first message to single line\n\t\t\tconst normalizedMessage = session.firstMessage.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\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\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadata = ` ${modified} · ${msgCount}`;\n\t\t\tconst metadataLine = theme.fg(\"dim\", truncateToWidth(metadata, width, \"\"));\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\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\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\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 (isEscape(keyData)) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - exit process\n\t\telse if (isCtrlC(keyData)) {\n\t\t\tprocess.exit(0);\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\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container {\n\tprivate sessionList: SessionList;\n\n\tconstructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Load all sessions\n\t\tconst sessions = sessionManager.loadAllSessions();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Resume Session\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list\n\t\tthis.sessionList = new SessionList(sessions);\n\t\tthis.sessionList.onSelect = onSelect;\n\t\tthis.sessionList.onCancel = onCancel;\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// Auto-cancel if no sessions\n\t\tif (sessions.length === 0) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,SAAS,EAOT,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAsB3E,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,UAAU,CAAC,CAAM;IACzB,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,UAAU,CAAC,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAC,CAIb;IAEF,YAAY,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,GAAE,oBAAyB,EAAE,UAAU,CAAC,EAAE,eAAe,EAqBxG;IAED,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAG1B;IAED,YAAY,CACX,MAAM,EAAE;QACP,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClF,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;KACjB,EACD,SAAS,UAAQ,GACf,IAAI,CAIN;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjC;IAED,OAAO,CAAC,aAAa;IAuFrB,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,mBAAmB;CA8R3B","sourcesContent":["import * as os from \"node:os\";\nimport {\n\tBox,\n\tContainer,\n\tgetCapabilities,\n\tgetImageDimensions,\n\tImage,\n\timageFallback,\n\tSpacer,\n\tText,\n} from \"@mariozechner/pi-tui\";\nimport stripAnsi from \"strip-ansi\";\nimport type { CustomAgentTool } from \"../../../core/custom-tools/types.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from \"../../../core/tools/truncate.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Convert absolute path to tilde notation if it's in home directory\n */\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (path.startsWith(home)) {\n\t\treturn \"~\" + path.slice(home.length);\n\t}\n\treturn path;\n}\n\n/**\n * Replace tabs with spaces for consistent rendering\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean; // default: true (only used if terminal supports images)\n}\n\n/**\n * Component that renders a tool call with its result (updateable)\n */\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox?: Box; // Only used for custom tools\n\tprivate contentText: Text; // For built-in tools (with its own padding/bg)\n\tprivate imageComponents: Image[] = [];\n\tprivate toolName: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate isPartial = true;\n\tprivate customTool?: CustomAgentTool;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\n\tconstructor(toolName: string, args: any, options: ToolExecutionOptions = {}, customTool?: CustomAgentTool) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.args = args;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.customTool = customTool;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (customTool) {\n\t\t\t// Custom tools use Box for flexible component rendering\n\t\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\t\tthis.addChild(this.contentBox);\n\t\t\tthis.contentText = new Text(\"\", 0, 0); // Fallback only\n\t\t} else {\n\t\t\t// Built-in tools use Text directly (has caching, better perf)\n\t\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Set background based on state\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\t// Check for custom tool rendering\n\t\tif (this.customTool && this.contentBox) {\n\t\t\t// Custom tools use Box for flexible component rendering\n\t\t\tthis.contentBox.setBgFn(bgFn);\n\t\t\tthis.contentBox.clear();\n\n\t\t\t// Render call component\n\t\t\tif (this.customTool.renderCall) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callComponent = this.customTool.renderCall(this.args, theme);\n\t\t\t\t\tif (callComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(callComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to default on error\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No custom renderCall, show tool name\n\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t}\n\n\t\t\t// Render result component if we have a result\n\t\t\tif (this.result && this.customTool.renderResult) {\n\t\t\t\ttry {\n\t\t\t\t\tconst resultComponent = this.customTool.renderResult(\n\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t);\n\t\t\t\t\tif (resultComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(resultComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to showing raw output on error\n\t\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\t\tif (output) {\n\t\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (this.result) {\n\t\t\t\t// Has result but no custom renderResult\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Built-in tools: use Text directly with caching\n\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t}\n\n\t\t// Handle images (same for both custom and built-in)\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\t\t\tconst caps = getCapabilities();\n\n\t\t\tfor (const img of imageBlocks) {\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tthis.addChild(new Spacer(1));\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timg.data,\n\t\t\t\t\t\timg.mimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: 60 },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\tif (!this.result) return \"\";\n\n\t\tconst textBlocks = this.result.content?.filter((c: any) => c.type === \"text\") || [];\n\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\n\t\tlet output = textBlocks\n\t\t\t.map((c: any) => {\n\t\t\t\tlet text = stripAnsi(c.text || \"\").replace(/\\r/g, \"\");\n\t\t\t\ttext = text.replace(/\\x1b./g, \"\");\n\t\t\t\ttext = text.replace(/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\x9f]/g, \"\");\n\t\t\t\treturn text;\n\t\t\t})\n\t\t\t.join(\"\\n\");\n\n\t\tconst caps = getCapabilities();\n\t\tif (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {\n\t\t\tconst imageIndicators = imageBlocks\n\t\t\t\t.map((img: any) => {\n\t\t\t\t\tconst dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;\n\t\t\t\t\treturn imageFallback(img.mimeType, dims);\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\toutput = output ? `${output}\\n${imageIndicators}` : imageIndicators;\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = \"\";\n\n\t\tif (this.toolName === \"bash\") {\n\t\t\tconst command = this.args?.command || \"\";\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(`$ ${command || theme.fg(\"toolOutput\", \"...\")}`));\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 5;\n\t\t\t\t\tconst skipped = Math.max(0, lines.length - maxLines);\n\t\t\t\t\tconst displayLines = lines.slice(-maxLines);\n\n\t\t\t\t\tif (skipped > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n\\n... (${skipped} earlier lines)`);\n\t\t\t\t\t}\n\t\t\t\t\ttext +=\n\t\t\t\t\t\t(skipped > 0 ? \"\\n\" : \"\\n\\n\") +\n\t\t\t\t\t\tdisplayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t}\n\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tconst fullOutputPath = this.result.details?.fullOutputPath;\n\t\t\t\tif (truncation?.truncated || fullOutputPath) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (fullOutputPath) {\n\t\t\t\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\twarnings.push(\n\t\t\t\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[${warnings.join(\". \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"read\") {\n\t\t\tconst path = shortenPath(this.args?.file_path || this.args?.path || \"\");\n\t\t\tconst offset = this.args?.offset;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\tlet pathDisplay = path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\t\t\tif (offset !== undefined || limit !== undefined) {\n\t\t\t\tconst startLine = offset ?? 1;\n\t\t\t\tconst endLine = limit !== undefined ? startLine + limit - 1 : \"\";\n\t\t\t\tpathDisplay += theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n\t\t\t}\n\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(\"read\")) + \" \" + pathDisplay;\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tconst lines = output.split(\"\\n\");\n\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", replaceTabs(line))).join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t}\n\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"write\") {\n\t\t\tconst path = shortenPath(this.args?.file_path || this.args?.path || \"\");\n\t\t\tconst fileContent = this.args?.content || \"\";\n\t\t\tconst lines = fileContent ? fileContent.split(\"\\n\") : [];\n\t\t\tconst totalLines = lines.length;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"write\")) +\n\t\t\t\t\" \" +\n\t\t\t\t(path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\"));\n\t\t\tif (totalLines > 10) {\n\t\t\t\ttext += ` (${totalLines} lines)`;\n\t\t\t}\n\n\t\t\tif (fileContent) {\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", replaceTabs(line))).join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"edit\") {\n\t\t\tconst path = shortenPath(this.args?.file_path || this.args?.path || \"\");\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"edit\")) +\n\t\t\t\t\" \" +\n\t\t\t\t(path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\"));\n\n\t\t\tif (this.result) {\n\t\t\t\tif (this.result.isError) {\n\t\t\t\t\tconst errorText = this.getTextOutput();\n\t\t\t\t\tif (errorText) {\n\t\t\t\t\t\ttext += \"\\n\\n\" + theme.fg(\"error\", errorText);\n\t\t\t\t\t}\n\t\t\t\t} else if (this.result.details?.diff) {\n\t\t\t\t\tconst diffLines = this.result.details.diff.split(\"\\n\");\n\t\t\t\t\tconst coloredLines = diffLines.map((line: string) => {\n\t\t\t\t\t\tif (line.startsWith(\"+\")) {\n\t\t\t\t\t\t\treturn theme.fg(\"toolDiffAdded\", line);\n\t\t\t\t\t\t} else if (line.startsWith(\"-\")) {\n\t\t\t\t\t\t\treturn theme.fg(\"toolDiffRemoved\", line);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn theme.fg(\"toolDiffContext\", line);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\ttext += \"\\n\\n\" + coloredLines.join(\"\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"ls\") {\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(\"ls\")) + \" \" + theme.fg(\"accent\", path);\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst entryLimit = this.result.details?.entryLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (entryLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (entryLimit) {\n\t\t\t\t\t\twarnings.push(`${entryLimit} entries limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"find\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"find\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", pattern) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst resultLimit = this.result.details?.resultLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (resultLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (resultLimit) {\n\t\t\t\t\t\twarnings.push(`${resultLimit} results limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"grep\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst glob = this.args?.glob;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", `/${pattern}/`) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (glob) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (${glob})`);\n\t\t\t}\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 15;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst matchLimit = this.result.details?.matchLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tconst linesTruncated = this.result.details?.linesTruncated;\n\t\t\t\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (matchLimit) {\n\t\t\t\t\t\twarnings.push(`${matchLimit} matches limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\twarnings.push(\"some lines truncated\");\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Generic tool (shouldn't reach here for custom tools)\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\n\t\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\t\ttext += \"\\n\\n\" + content;\n\t\t\tconst output = this.getTextOutput();\n\t\t\tif (output) {\n\t\t\t\ttext += \"\\n\" + output;\n\t\t\t}\n\t\t}\n\n\t\treturn text;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,SAAS,EAOT,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAuB3E,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,UAAU,CAAC,CAAM;IACzB,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,UAAU,CAAC,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAC,CAIb;IAEF,YAAY,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,GAAE,oBAAyB,EAAE,UAAU,CAAC,EAAE,eAAe,EAqBxG;IAED,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAG1B;IAED,YAAY,CACX,MAAM,EAAE;QACP,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClF,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;KACjB,EACD,SAAS,UAAQ,GACf,IAAI,CAIN;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjC;IAED,OAAO,CAAC,aAAa;IAuFrB,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,mBAAmB;CAqS3B","sourcesContent":["import * as os from \"node:os\";\nimport {\n\tBox,\n\tContainer,\n\tgetCapabilities,\n\tgetImageDimensions,\n\tImage,\n\timageFallback,\n\tSpacer,\n\tText,\n} from \"@mariozechner/pi-tui\";\nimport stripAnsi from \"strip-ansi\";\nimport type { CustomAgentTool } from \"../../../core/custom-tools/types.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from \"../../../core/tools/truncate.js\";\nimport { getLanguageFromPath, highlightCode, theme } from \"../theme/theme.js\";\nimport { renderDiff } from \"./diff.js\";\n\n/**\n * Convert absolute path to tilde notation if it's in home directory\n */\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (path.startsWith(home)) {\n\t\treturn \"~\" + path.slice(home.length);\n\t}\n\treturn path;\n}\n\n/**\n * Replace tabs with spaces for consistent rendering\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean; // default: true (only used if terminal supports images)\n}\n\n/**\n * Component that renders a tool call with its result (updateable)\n */\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox?: Box; // Only used for custom tools\n\tprivate contentText: Text; // For built-in tools (with its own padding/bg)\n\tprivate imageComponents: Image[] = [];\n\tprivate toolName: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate isPartial = true;\n\tprivate customTool?: CustomAgentTool;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\n\tconstructor(toolName: string, args: any, options: ToolExecutionOptions = {}, customTool?: CustomAgentTool) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.args = args;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.customTool = customTool;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (customTool) {\n\t\t\t// Custom tools use Box for flexible component rendering\n\t\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\t\tthis.addChild(this.contentBox);\n\t\t\tthis.contentText = new Text(\"\", 0, 0); // Fallback only\n\t\t} else {\n\t\t\t// Built-in tools use Text directly (has caching, better perf)\n\t\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Set background based on state\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\t// Check for custom tool rendering\n\t\tif (this.customTool && this.contentBox) {\n\t\t\t// Custom tools use Box for flexible component rendering\n\t\t\tthis.contentBox.setBgFn(bgFn);\n\t\t\tthis.contentBox.clear();\n\n\t\t\t// Render call component\n\t\t\tif (this.customTool.renderCall) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callComponent = this.customTool.renderCall(this.args, theme);\n\t\t\t\t\tif (callComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(callComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to default on error\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No custom renderCall, show tool name\n\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t}\n\n\t\t\t// Render result component if we have a result\n\t\t\tif (this.result && this.customTool.renderResult) {\n\t\t\t\ttry {\n\t\t\t\t\tconst resultComponent = this.customTool.renderResult(\n\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t);\n\t\t\t\t\tif (resultComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(resultComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to showing raw output on error\n\t\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\t\tif (output) {\n\t\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (this.result) {\n\t\t\t\t// Has result but no custom renderResult\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Built-in tools: use Text directly with caching\n\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t}\n\n\t\t// Handle images (same for both custom and built-in)\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\t\t\tconst caps = getCapabilities();\n\n\t\t\tfor (const img of imageBlocks) {\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tthis.addChild(new Spacer(1));\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timg.data,\n\t\t\t\t\t\timg.mimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: 60 },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\tif (!this.result) return \"\";\n\n\t\tconst textBlocks = this.result.content?.filter((c: any) => c.type === \"text\") || [];\n\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\n\t\tlet output = textBlocks\n\t\t\t.map((c: any) => {\n\t\t\t\tlet text = stripAnsi(c.text || \"\").replace(/\\r/g, \"\");\n\t\t\t\ttext = text.replace(/\\x1b./g, \"\");\n\t\t\t\ttext = text.replace(/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\x9f]/g, \"\");\n\t\t\t\treturn text;\n\t\t\t})\n\t\t\t.join(\"\\n\");\n\n\t\tconst caps = getCapabilities();\n\t\tif (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {\n\t\t\tconst imageIndicators = imageBlocks\n\t\t\t\t.map((img: any) => {\n\t\t\t\t\tconst dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;\n\t\t\t\t\treturn imageFallback(img.mimeType, dims);\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\toutput = output ? `${output}\\n${imageIndicators}` : imageIndicators;\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = \"\";\n\n\t\tif (this.toolName === \"bash\") {\n\t\t\tconst command = this.args?.command || \"\";\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(`$ ${command || theme.fg(\"toolOutput\", \"...\")}`));\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 5;\n\t\t\t\t\tconst skipped = Math.max(0, lines.length - maxLines);\n\t\t\t\t\tconst displayLines = lines.slice(-maxLines);\n\n\t\t\t\t\tif (skipped > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n\\n... (${skipped} earlier lines)`);\n\t\t\t\t\t}\n\t\t\t\t\ttext +=\n\t\t\t\t\t\t(skipped > 0 ? \"\\n\" : \"\\n\\n\") +\n\t\t\t\t\t\tdisplayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t}\n\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tconst fullOutputPath = this.result.details?.fullOutputPath;\n\t\t\t\tif (truncation?.truncated || fullOutputPath) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (fullOutputPath) {\n\t\t\t\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\twarnings.push(\n\t\t\t\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[${warnings.join(\". \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"read\") {\n\t\t\tconst path = shortenPath(this.args?.file_path || this.args?.path || \"\");\n\t\t\tconst offset = this.args?.offset;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\tlet pathDisplay = path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\t\t\tif (offset !== undefined || limit !== undefined) {\n\t\t\t\tconst startLine = offset ?? 1;\n\t\t\t\tconst endLine = limit !== undefined ? startLine + limit - 1 : \"\";\n\t\t\t\tpathDisplay += theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n\t\t\t}\n\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(\"read\")) + \" \" + pathDisplay;\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\t\tconst lang = getLanguageFromPath(rawPath);\n\t\t\t\tconst lines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext +=\n\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\tdisplayLines\n\t\t\t\t\t\t.map((line: string) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line))))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t}\n\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"write\") {\n\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\tconst path = shortenPath(rawPath);\n\t\t\tconst fileContent = this.args?.content || \"\";\n\t\t\tconst lang = getLanguageFromPath(rawPath);\n\t\t\tconst lines = fileContent\n\t\t\t\t? lang\n\t\t\t\t\t? highlightCode(replaceTabs(fileContent), lang)\n\t\t\t\t\t: fileContent.split(\"\\n\")\n\t\t\t\t: [];\n\t\t\tconst totalLines = lines.length;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"write\")) +\n\t\t\t\t\" \" +\n\t\t\t\t(path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\"));\n\t\t\tif (totalLines > 10) {\n\t\t\t\ttext += ` (${totalLines} lines)`;\n\t\t\t}\n\n\t\t\tif (fileContent) {\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext +=\n\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\tdisplayLines\n\t\t\t\t\t\t.map((line: string) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line))))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"edit\") {\n\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\tconst path = shortenPath(rawPath);\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"edit\")) +\n\t\t\t\t\" \" +\n\t\t\t\t(path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\"));\n\n\t\t\tif (this.result) {\n\t\t\t\tif (this.result.isError) {\n\t\t\t\t\tconst errorText = this.getTextOutput();\n\t\t\t\t\tif (errorText) {\n\t\t\t\t\t\ttext += \"\\n\\n\" + theme.fg(\"error\", errorText);\n\t\t\t\t\t}\n\t\t\t\t} else if (this.result.details?.diff) {\n\t\t\t\t\ttext += \"\\n\\n\" + renderDiff(this.result.details.diff, { filePath: rawPath });\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"ls\") {\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(\"ls\")) + \" \" + theme.fg(\"accent\", path);\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst entryLimit = this.result.details?.entryLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (entryLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (entryLimit) {\n\t\t\t\t\t\twarnings.push(`${entryLimit} entries limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"find\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"find\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", pattern) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst resultLimit = this.result.details?.resultLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (resultLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (resultLimit) {\n\t\t\t\t\t\twarnings.push(`${resultLimit} results limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"grep\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst glob = this.args?.glob;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", `/${pattern}/`) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (glob) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (${glob})`);\n\t\t\t}\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 15;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += \"\\n\\n\" + displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst matchLimit = this.result.details?.matchLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tconst linesTruncated = this.result.details?.linesTruncated;\n\t\t\t\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (matchLimit) {\n\t\t\t\t\t\twarnings.push(`${matchLimit} matches limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\twarnings.push(\"some lines truncated\");\n\t\t\t\t\t}\n\t\t\t\t\ttext += \"\\n\" + theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Generic tool (shouldn't reach here for custom tools)\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\n\t\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\t\ttext += \"\\n\\n\" + content;\n\t\t\tconst output = this.getTextOutput();\n\t\t\tif (output) {\n\t\t\t\ttext += \"\\n\" + output;\n\t\t\t}\n\t\t}\n\n\t\treturn text;\n\t}\n}\n"]}
@@ -2,7 +2,8 @@ import * as os from "node:os";
2
2
  import { Box, Container, getCapabilities, getImageDimensions, Image, imageFallback, Spacer, Text, } from "@mariozechner/pi-tui";
3
3
  import stripAnsi from "strip-ansi";
4
4
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
5
- import { theme } from "../theme/theme.js";
5
+ import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
6
+ import { renderDiff } from "./diff.js";
6
7
  /**
7
8
  * Convert absolute path to tilde notation if it's in home directory
8
9
  */
@@ -222,11 +223,17 @@ export class ToolExecutionComponent extends Container {
222
223
  text = theme.fg("toolTitle", theme.bold("read")) + " " + pathDisplay;
223
224
  if (this.result) {
224
225
  const output = this.getTextOutput();
225
- const lines = output.split("\n");
226
+ const rawPath = this.args?.file_path || this.args?.path || "";
227
+ const lang = getLanguageFromPath(rawPath);
228
+ const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
226
229
  const maxLines = this.expanded ? lines.length : 10;
227
230
  const displayLines = lines.slice(0, maxLines);
228
231
  const remaining = lines.length - maxLines;
229
- text += "\n\n" + displayLines.map((line) => theme.fg("toolOutput", replaceTabs(line))).join("\n");
232
+ text +=
233
+ "\n\n" +
234
+ displayLines
235
+ .map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
236
+ .join("\n");
230
237
  if (remaining > 0) {
231
238
  text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
232
239
  }
@@ -251,9 +258,15 @@ export class ToolExecutionComponent extends Container {
251
258
  }
252
259
  }
253
260
  else if (this.toolName === "write") {
254
- const path = shortenPath(this.args?.file_path || this.args?.path || "");
261
+ const rawPath = this.args?.file_path || this.args?.path || "";
262
+ const path = shortenPath(rawPath);
255
263
  const fileContent = this.args?.content || "";
256
- const lines = fileContent ? fileContent.split("\n") : [];
264
+ const lang = getLanguageFromPath(rawPath);
265
+ const lines = fileContent
266
+ ? lang
267
+ ? highlightCode(replaceTabs(fileContent), lang)
268
+ : fileContent.split("\n")
269
+ : [];
257
270
  const totalLines = lines.length;
258
271
  text =
259
272
  theme.fg("toolTitle", theme.bold("write")) +
@@ -266,14 +279,19 @@ export class ToolExecutionComponent extends Container {
266
279
  const maxLines = this.expanded ? lines.length : 10;
267
280
  const displayLines = lines.slice(0, maxLines);
268
281
  const remaining = lines.length - maxLines;
269
- text += "\n\n" + displayLines.map((line) => theme.fg("toolOutput", replaceTabs(line))).join("\n");
282
+ text +=
283
+ "\n\n" +
284
+ displayLines
285
+ .map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
286
+ .join("\n");
270
287
  if (remaining > 0) {
271
288
  text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
272
289
  }
273
290
  }
274
291
  }
275
292
  else if (this.toolName === "edit") {
276
- const path = shortenPath(this.args?.file_path || this.args?.path || "");
293
+ const rawPath = this.args?.file_path || this.args?.path || "";
294
+ const path = shortenPath(rawPath);
277
295
  text =
278
296
  theme.fg("toolTitle", theme.bold("edit")) +
279
297
  " " +
@@ -286,19 +304,7 @@ export class ToolExecutionComponent extends Container {
286
304
  }
287
305
  }
288
306
  else if (this.result.details?.diff) {
289
- const diffLines = this.result.details.diff.split("\n");
290
- const coloredLines = diffLines.map((line) => {
291
- if (line.startsWith("+")) {
292
- return theme.fg("toolDiffAdded", line);
293
- }
294
- else if (line.startsWith("-")) {
295
- return theme.fg("toolDiffRemoved", line);
296
- }
297
- else {
298
- return theme.fg("toolDiffContext", line);
299
- }
300
- });
301
- text += "\n\n" + coloredLines.join("\n");
307
+ text += "\n\n" + renderDiff(this.result.details.diff, { filePath: rawPath });
302
308
  }
303
309
  }
304
310
  }