@mariozechner/pi-coding-agent 0.12.4 → 0.12.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 (50) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +25 -1
  3. package/dist/changelog.d.ts +1 -1
  4. package/dist/changelog.d.ts.map +1 -1
  5. package/dist/changelog.js +1 -1
  6. package/dist/changelog.js.map +1 -1
  7. package/dist/config.d.ts +48 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +109 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/export-html.d.ts.map +1 -1
  12. package/dist/export-html.js +1 -4
  13. package/dist/export-html.js.map +1 -1
  14. package/dist/main.d.ts.map +1 -1
  15. package/dist/main.js +23 -27
  16. package/dist/main.js.map +1 -1
  17. package/dist/model-config.d.ts.map +1 -1
  18. package/dist/model-config.js +3 -4
  19. package/dist/model-config.js.map +1 -1
  20. package/dist/oauth/storage.d.ts.map +1 -1
  21. package/dist/oauth/storage.js +4 -12
  22. package/dist/oauth/storage.js.map +1 -1
  23. package/dist/session-manager.d.ts +2 -2
  24. package/dist/session-manager.d.ts.map +1 -1
  25. package/dist/session-manager.js +2 -2
  26. package/dist/session-manager.js.map +1 -1
  27. package/dist/settings-manager.d.ts.map +1 -1
  28. package/dist/settings-manager.js +2 -2
  29. package/dist/settings-manager.js.map +1 -1
  30. package/dist/slash-commands.d.ts +2 -2
  31. package/dist/slash-commands.d.ts.map +1 -1
  32. package/dist/slash-commands.js +7 -8
  33. package/dist/slash-commands.js.map +1 -1
  34. package/dist/theme/theme.d.ts.map +1 -1
  35. package/dist/theme/theme.js +11 -15
  36. package/dist/theme/theme.js.map +1 -1
  37. package/dist/tools-manager.d.ts.map +1 -1
  38. package/dist/tools-manager.js +3 -2
  39. package/dist/tools-manager.js.map +1 -1
  40. package/dist/tui/model-selector.d.ts.map +1 -1
  41. package/dist/tui/model-selector.js +1 -1
  42. package/dist/tui/model-selector.js.map +1 -1
  43. package/dist/tui/tui-renderer.d.ts.map +1 -1
  44. package/dist/tui/tui-renderer.js +8 -8
  45. package/dist/tui/tui-renderer.js.map +1 -1
  46. package/package.json +8 -4
  47. package/dist/paths.d.ts +0 -32
  48. package/dist/paths.d.ts.map +0 -1
  49. package/dist/paths.js +0 -60
  50. package/dist/paths.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.js","sourceRoot":"","sources":["../../src/tui/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 \"../model-config.js\";\nimport type { SettingsManager } from \"../settings-manager.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 ~/.pi/agent/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\tif (!query.trim()) {\n\t\t\tthis.filteredModels = this.allModels;\n\t\t} else {\n\t\t\tconst searchTokens = query\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(/\\s+/)\n\t\t\t\t.filter((t) => t);\n\t\t\tthis.filteredModels = this.allModels.filter(({ provider, id, model }) => {\n\t\t\t\tconst searchText = `${provider} ${id} ${model.name}`.toLowerCase();\n\t\t\t\treturn searchTokens.every((token) => searchText.includes(token));\n\t\t\t});\n\t\t}\n\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/tui/model-selector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAY,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAQpD;;GAEG;AACH,MAAM,OAAO,sBAAuB,SAAQ,SAAS;IAC5C,WAAW,CAAQ;IACnB,aAAa,CAAY;IACzB,SAAS,GAAgB,EAAE,CAAC;IAC5B,cAAc,GAAgB,EAAE,CAAC;IACjC,aAAa,GAAW,CAAC,CAAC;IAC1B,YAAY,CAAoB;IAChC,eAAe,CAAkB;IACjC,gBAAgB,CAA8B;IAC9C,gBAAgB,CAAa;IAC7B,YAAY,GAAkB,IAAI,CAAC;IACnC,GAAG,CAAM;IAEjB,YACC,GAAQ,EACR,YAA+B,EAC/B,eAAgC,EAChC,QAAqC,EACrC,QAAoB,EACnB;QACD,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QACjC,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QAEjC,iBAAiB;QACjB,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,mCAAmC;QACnC,IAAI,CAAC,QAAQ,CACZ,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,uEAAuE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAC5G,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,sBAAsB;QACtB,IAAI,CAAC,WAAW,GAAG,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,WAAW,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC;YACjC,wDAAwD;YACxD,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC7C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,CAAC;YAClE,CAAC;QAAA,CACD,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAEhC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,wBAAwB;QACxB,IAAI,CAAC,aAAa,GAAG,IAAI,SAAS,EAAE,CAAC;QACrC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAElC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,oBAAoB;QACpB,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;QAEnC,oCAAoC;QACpC,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YAC5B,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,4CAA4C;YAC5C,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QAAA,CACzB,CAAC,CAAC;IAAA,CACH;IAEO,KAAK,CAAC,UAAU,GAAkB;QACzC,wEAAwE;QACxE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,GAAG,MAAM,kBAAkB,EAAE,CAAC;QAEtE,kFAAkF;QAClF,0CAA0C;QAC1C,IAAI,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;YACpB,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;YACzB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;YAC1B,OAAO;QACR,CAAC;QAED,MAAM,MAAM,GAAgB,eAAe,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC3D,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,KAAK;SACL,CAAC,CAAC,CAAC;QAEJ,8CAA8C;QAC9C,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACrB,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,YAAY,EAAE,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC;YACtG,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,YAAY,EAAE,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC;YACtG,IAAI,UAAU,IAAI,CAAC,UAAU;gBAAE,OAAO,CAAC,CAAC,CAAC;YACzC,IAAI,CAAC,UAAU,IAAI,UAAU;gBAAE,OAAO,CAAC,CAAC;YACxC,OAAO,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAA,CAC5C,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QACxB,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC;IAAA,CAC7B;IAEO,YAAY,CAAC,KAAa,EAAQ;QACzC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC;QACtC,CAAC;aAAM,CAAC;YACP,MAAM,YAAY,GAAG,KAAK;iBACxB,WAAW,EAAE;iBACb,KAAK,CAAC,KAAK,CAAC;iBACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;YACnB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;gBACxE,MAAM,UAAU,GAAG,GAAG,QAAQ,IAAI,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;gBACnE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;YAAA,CACjE,CAAC,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/F,IAAI,CAAC,UAAU,EAAE,CAAC;IAAA,CAClB;IAEO,UAAU,GAAS;QAC1B,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;QAE3B,MAAM,UAAU,GAAG,EAAE,CAAC;QACtB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAC1B,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,UAAU,CAAC,CAClG,CAAC;QACF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAE/E,wCAAwC;QACxC,KAAK,IAAI,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI;gBAAE,SAAS;YAEpB,MAAM,UAAU,GAAG,CAAC,KAAK,IAAI,CAAC,aAAa,CAAC;YAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAE1D,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,IAAI,UAAU,EAAE,CAAC;gBAChB,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAI,CAAC,CAAC;gBACxC,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;gBAC/B,MAAM,aAAa,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAC9D,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7D,IAAI,GAAG,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,GAAG,GAAG,aAAa,GAAG,SAAS,CAAC;YACjF,CAAC;iBAAM,CAAC;gBACP,MAAM,SAAS,GAAG,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;gBACjC,MAAM,aAAa,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAC9D,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7D,IAAI,GAAG,SAAS,GAAG,GAAG,GAAG,aAAa,GAAG,SAAS,CAAC;YACpD,CAAC;YAED,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;QAED,iCAAiC;QACjC,IAAI,UAAU,GAAG,CAAC,IAAI,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;YAC7D,MAAM,UAAU,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,aAAa,GAAG,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;YACpG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;QAED,8CAA8C;QAC9C,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,oBAAoB;YACpB,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;gBAC/B,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACtE,CAAC;QACF,CAAC;aAAM,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACxF,CAAC;IAAA,CACD;IAED,WAAW,CAAC,OAAe,EAAQ;QAClC,wCAAwC;QACxC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC1B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACxG,IAAI,CAAC,UAAU,EAAE,CAAC;QACnB,CAAC;QACD,0CAA0C;aACrC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACxG,IAAI,CAAC,UAAU,EAAE,CAAC;QACnB,CAAC;QACD,QAAQ;aACH,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YAC3B,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC9D,IAAI,aAAa,EAAE,CAAC;gBACnB,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC;QACF,CAAC;QACD,SAAS;aACJ,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YAC7B,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACzB,CAAC;QACD,uCAAuC;aAClC,CAAC;YACL,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC;QAChD,CAAC;IAAA,CACD;IAEO,YAAY,CAAC,KAAiB,EAAQ;QAC7C,sBAAsB;QACtB,IAAI,CAAC,eAAe,CAAC,0BAA0B,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAAA,CAC7B;IAED,cAAc,GAAU;QACvB,OAAO,IAAI,CAAC,WAAW,CAAC;IAAA,CACxB;CACD","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../model-config.js\";\nimport type { SettingsManager } from \"../settings-manager.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\tif (!query.trim()) {\n\t\t\tthis.filteredModels = this.allModels;\n\t\t} else {\n\t\t\tconst searchTokens = query\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(/\\s+/)\n\t\t\t\t.filter((t) => t);\n\t\t\tthis.filteredModels = this.allModels.filter(({ provider, id, model }) => {\n\t\t\t\tconst searchText = `${provider} ${id} ${model.name}`.toLowerCase();\n\t\t\t\treturn searchTokens.every((token) => searchText.includes(token));\n\t\t\t});\n\t\t}\n\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 +1 @@
1
- {"version":3,"file":"tui-renderer.d.ts","sourceRoot":"","sources":["../../src/tui/tui-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Agent, AgentEvent, AgentState, ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Message, Model } from \"@mariozechner/pi-ai\";\nimport type { SlashCommand } from \"@mariozechner/pi-tui\";\nimport {\n\tCombinedAutocompleteProvider,\n\tContainer,\n\tInput,\n\tLoader,\n\tMarkdown,\n\tProcessTerminal,\n\tSpacer,\n\tText,\n\tTruncatedText,\n\tTUI,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport { exec } from \"child_process\";\nimport { getChangelogPath, parseChangelog } from \"../changelog.js\";\nimport { exportSessionToHtml } from \"../export-html.js\";\nimport { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from \"../model-config.js\";\nimport { listOAuthProviders, login, logout } from \"../oauth/index.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type { SettingsManager } from \"../settings-manager.js\";\nimport { expandSlashCommand, type FileSlashCommand, loadSlashCommands } from \"../slash-commands.js\";\nimport { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from \"../theme/theme.js\";\nimport { AssistantMessageComponent } from \"./assistant-message.js\";\nimport { CustomEditor } from \"./custom-editor.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\nimport { FooterComponent } from \"./footer.js\";\nimport { ModelSelectorComponent } from \"./model-selector.js\";\nimport { OAuthSelectorComponent } from \"./oauth-selector.js\";\nimport { QueueModeSelectorComponent } from \"./queue-mode-selector.js\";\nimport { ThemeSelectorComponent } from \"./theme-selector.js\";\nimport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nimport { ToolExecutionComponent } from \"./tool-execution.js\";\nimport { UserMessageComponent } from \"./user-message.js\";\nimport { UserMessageSelectorComponent } from \"./user-message-selector.js\";\n\n/**\n * TUI renderer for the coding agent\n */\nexport class TuiRenderer {\n\tprivate ui: TUI;\n\tprivate chatContainer: Container;\n\tprivate pendingMessagesContainer: Container;\n\tprivate statusContainer: Container;\n\tprivate editor: CustomEditor;\n\tprivate editorContainer: Container; // Container to swap between editor and selector\n\tprivate footer: FooterComponent;\n\tprivate agent: Agent;\n\tprivate sessionManager: SessionManager;\n\tprivate settingsManager: SettingsManager;\n\tprivate version: string;\n\tprivate isInitialized = false;\n\tprivate onInputCallback?: (text: string) => void;\n\tprivate loadingAnimation: Loader | null = null;\n\n\tprivate lastSigintTime = 0;\n\tprivate changelogMarkdown: string | null = null;\n\tprivate newVersion: string | null = null;\n\n\t// Message queueing\n\tprivate queuedMessages: string[] = [];\n\n\t// Streaming message tracking\n\tprivate streamingComponent: AssistantMessageComponent | null = null;\n\n\t// Tool execution tracking: toolCallId -> component\n\tprivate pendingTools = new Map<string, ToolExecutionComponent>();\n\n\t// Thinking level selector\n\tprivate thinkingSelector: ThinkingSelectorComponent | null = null;\n\n\t// Queue mode selector\n\tprivate queueModeSelector: QueueModeSelectorComponent | null = null;\n\n\t// Theme selector\n\tprivate themeSelector: ThemeSelectorComponent | null = null;\n\n\t// Model selector\n\tprivate modelSelector: ModelSelectorComponent | null = null;\n\n\t// User message selector (for branching)\n\tprivate userMessageSelector: UserMessageSelectorComponent | null = null;\n\n\t// OAuth selector\n\tprivate oauthSelector: any | null = null;\n\n\t// Track if this is the first user message (to skip spacer)\n\tprivate isFirstUserMessage = true;\n\n\t// Model scope for quick cycling\n\tprivate scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }> = [];\n\n\t// Tool output expansion state\n\tprivate toolOutputExpanded = false;\n\n\t// Agent subscription unsubscribe function\n\tprivate unsubscribe?: () => void;\n\n\t// File-based slash commands\n\tprivate fileCommands: FileSlashCommand[] = [];\n\n\tconstructor(\n\t\tagent: Agent,\n\t\tsessionManager: SessionManager,\n\t\tsettingsManager: SettingsManager,\n\t\tversion: string,\n\t\tchangelogMarkdown: string | null = null,\n\t\tnewVersion: string | null = null,\n\t\tscopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }> = [],\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.version = version;\n\t\tthis.newVersion = newVersion;\n\t\tthis.changelogMarkdown = changelogMarkdown;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.ui = new TUI(new ProcessTerminal());\n\t\tthis.chatContainer = new Container();\n\t\tthis.pendingMessagesContainer = new Container();\n\t\tthis.statusContainer = new Container();\n\t\tthis.editor = new CustomEditor(getEditorTheme());\n\t\tthis.editorContainer = new Container(); // Container to hold editor or selector\n\t\tthis.editorContainer.addChild(this.editor); // Start with editor\n\t\tthis.footer = new FooterComponent(agent.state);\n\n\t\t// Define slash commands\n\t\tconst thinkingCommand: SlashCommand = {\n\t\t\tname: \"thinking\",\n\t\t\tdescription: \"Select reasoning level (opens selector UI)\",\n\t\t};\n\n\t\tconst modelCommand: SlashCommand = {\n\t\t\tname: \"model\",\n\t\t\tdescription: \"Select model (opens selector UI)\",\n\t\t};\n\n\t\tconst exportCommand: SlashCommand = {\n\t\t\tname: \"export\",\n\t\t\tdescription: \"Export session to HTML file\",\n\t\t};\n\n\t\tconst sessionCommand: SlashCommand = {\n\t\t\tname: \"session\",\n\t\t\tdescription: \"Show session info and stats\",\n\t\t};\n\n\t\tconst changelogCommand: SlashCommand = {\n\t\t\tname: \"changelog\",\n\t\t\tdescription: \"Show changelog entries\",\n\t\t};\n\n\t\tconst branchCommand: SlashCommand = {\n\t\t\tname: \"branch\",\n\t\t\tdescription: \"Create a new branch from a previous message\",\n\t\t};\n\n\t\tconst loginCommand: SlashCommand = {\n\t\t\tname: \"login\",\n\t\t\tdescription: \"Login with OAuth provider\",\n\t\t};\n\n\t\tconst logoutCommand: SlashCommand = {\n\t\t\tname: \"logout\",\n\t\t\tdescription: \"Logout from OAuth provider\",\n\t\t};\n\n\t\tconst queueCommand: SlashCommand = {\n\t\t\tname: \"queue\",\n\t\t\tdescription: \"Select message queue mode (opens selector UI)\",\n\t\t};\n\n\t\tconst themeCommand: SlashCommand = {\n\t\t\tname: \"theme\",\n\t\t\tdescription: \"Select color theme (opens selector UI)\",\n\t\t};\n\n\t\tconst clearCommand: SlashCommand = {\n\t\t\tname: \"clear\",\n\t\t\tdescription: \"Clear context and start a fresh session\",\n\t\t};\n\n\t\t// Load file-based slash commands\n\t\tthis.fileCommands = loadSlashCommands();\n\n\t\t// Convert file commands to SlashCommand format\n\t\tconst fileSlashCommands: SlashCommand[] = this.fileCommands.map((cmd) => ({\n\t\t\tname: cmd.name,\n\t\t\tdescription: cmd.description,\n\t\t}));\n\n\t\t// Setup autocomplete for file paths and slash commands\n\t\tconst autocompleteProvider = new CombinedAutocompleteProvider(\n\t\t\t[\n\t\t\t\tthinkingCommand,\n\t\t\t\tmodelCommand,\n\t\t\t\tthemeCommand,\n\t\t\t\texportCommand,\n\t\t\t\tsessionCommand,\n\t\t\t\tchangelogCommand,\n\t\t\t\tbranchCommand,\n\t\t\t\tloginCommand,\n\t\t\t\tlogoutCommand,\n\t\t\t\tqueueCommand,\n\t\t\t\tclearCommand,\n\t\t\t\t...fileSlashCommands,\n\t\t\t],\n\t\t\tprocess.cwd(),\n\t\t\tfdPath,\n\t\t);\n\t\tthis.editor.setAutocompleteProvider(autocompleteProvider);\n\t}\n\n\tasync init(): Promise<void> {\n\t\tif (this.isInitialized) return;\n\n\t\t// Add header with logo and instructions\n\t\tconst logo = theme.bold(theme.fg(\"accent\", \"pi\")) + theme.fg(\"dim\", ` v${this.version}`);\n\t\tconst instructions =\n\t\t\ttheme.fg(\"dim\", \"esc\") +\n\t\t\ttheme.fg(\"muted\", \" to interrupt\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+c\") +\n\t\t\ttheme.fg(\"muted\", \" to clear\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+c twice\") +\n\t\t\ttheme.fg(\"muted\", \" to exit\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+k\") +\n\t\t\ttheme.fg(\"muted\", \" to delete line\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"shift+tab\") +\n\t\t\ttheme.fg(\"muted\", \" to cycle thinking\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+p\") +\n\t\t\ttheme.fg(\"muted\", \" to cycle models\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+o\") +\n\t\t\ttheme.fg(\"muted\", \" to expand tools\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"/\") +\n\t\t\ttheme.fg(\"muted\", \" for commands\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"drop files\") +\n\t\t\ttheme.fg(\"muted\", \" to attach\");\n\t\tconst header = new Text(logo + \"\\n\" + instructions, 1, 0);\n\n\t\t// Setup UI layout\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(header);\n\t\tthis.ui.addChild(new Spacer(1));\n\n\t\t// Add new version notification if available\n\t\tif (this.newVersion) {\n\t\t\tthis.ui.addChild(new DynamicBorder((text) => theme.fg(\"warning\", text)));\n\t\t\tthis.ui.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.bold(theme.fg(\"warning\", \"Update Available\")) +\n\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\ttheme.fg(\"muted\", `New version ${this.newVersion} is available. Run: `) +\n\t\t\t\t\t\ttheme.fg(\"accent\", \"npm install -g @mariozechner/pi-coding-agent\"),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ui.addChild(new DynamicBorder((text) => theme.fg(\"warning\", text)));\n\t\t}\n\n\t\t// Add changelog if provided\n\t\tif (this.changelogMarkdown) {\n\t\t\tthis.ui.addChild(new DynamicBorder());\n\t\t\tthis.ui.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\t\tthis.ui.addChild(new Spacer(1));\n\t\t\tthis.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));\n\t\t\tthis.ui.addChild(new Spacer(1));\n\t\t\tthis.ui.addChild(new DynamicBorder());\n\t\t}\n\n\t\tthis.ui.addChild(this.chatContainer);\n\t\tthis.ui.addChild(this.pendingMessagesContainer);\n\t\tthis.ui.addChild(this.statusContainer);\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(this.editorContainer); // Use container that can hold editor or selector\n\t\tthis.ui.addChild(this.footer);\n\t\tthis.ui.setFocus(this.editor);\n\n\t\t// Set up custom key handlers on the editor\n\t\tthis.editor.onEscape = () => {\n\t\t\t// Intercept Escape key when processing\n\t\t\tif (this.loadingAnimation) {\n\t\t\t\t// Get all queued messages\n\t\t\t\tconst queuedText = this.queuedMessages.join(\"\\n\\n\");\n\n\t\t\t\t// Get current editor text\n\t\t\t\tconst currentText = this.editor.getText();\n\n\t\t\t\t// Combine: queued messages + current editor text\n\t\t\t\tconst combinedText = [queuedText, currentText].filter((t) => t.trim()).join(\"\\n\\n\");\n\n\t\t\t\t// Put back in editor\n\t\t\t\tthis.editor.setText(combinedText);\n\n\t\t\t\t// Clear queued messages\n\t\t\t\tthis.queuedMessages = [];\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\n\t\t\t\t// Clear agent's queue too\n\t\t\t\tthis.agent.clearMessageQueue();\n\n\t\t\t\t// Abort\n\t\t\t\tthis.agent.abort();\n\t\t\t}\n\t\t};\n\n\t\tthis.editor.onCtrlC = () => {\n\t\t\tthis.handleCtrlC();\n\t\t};\n\n\t\tthis.editor.onShiftTab = () => {\n\t\t\tthis.cycleThinkingLevel();\n\t\t};\n\n\t\tthis.editor.onCtrlP = () => {\n\t\t\tthis.cycleModel();\n\t\t};\n\n\t\tthis.editor.onCtrlO = () => {\n\t\t\tthis.toggleToolOutputExpansion();\n\t\t};\n\n\t\t// Handle editor submission\n\t\tthis.editor.onSubmit = async (text: string) => {\n\t\t\ttext = text.trim();\n\t\t\tif (!text) return;\n\n\t\t\t// Check for /thinking command\n\t\t\tif (text === \"/thinking\") {\n\t\t\t\t// Show thinking level selector\n\t\t\t\tthis.showThinkingSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /model command\n\t\t\tif (text === \"/model\") {\n\t\t\t\t// Show model selector\n\t\t\t\tthis.showModelSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /export command\n\t\t\tif (text.startsWith(\"/export\")) {\n\t\t\t\tthis.handleExportCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /session command\n\t\t\tif (text === \"/session\") {\n\t\t\t\tthis.handleSessionCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /changelog command\n\t\t\tif (text === \"/changelog\") {\n\t\t\t\tthis.handleChangelogCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /branch command\n\t\t\tif (text === \"/branch\") {\n\t\t\t\tthis.showUserMessageSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /login command\n\t\t\tif (text === \"/login\") {\n\t\t\t\tthis.showOAuthSelector(\"login\");\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /logout command\n\t\t\tif (text === \"/logout\") {\n\t\t\t\tthis.showOAuthSelector(\"logout\");\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /queue command\n\t\t\tif (text === \"/queue\") {\n\t\t\t\tthis.showQueueModeSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /theme command\n\t\t\tif (text === \"/theme\") {\n\t\t\t\tthis.showThemeSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /clear command\n\t\t\tif (text === \"/clear\") {\n\t\t\t\tthis.handleClearCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /debug command\n\t\t\tif (text === \"/debug\") {\n\t\t\t\tthis.handleDebugCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for file-based slash commands\n\t\t\ttext = expandSlashCommand(text, this.fileCommands);\n\n\t\t\t// Normal message submission - validate model and API key first\n\t\t\tconst currentModel = this.agent.state.model;\n\t\t\tif (!currentModel) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t\"No model selected.\\n\\n\" +\n\t\t\t\t\t\t\"Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\\n\" +\n\t\t\t\t\t\t\"or create ~/.pi/agent/models.json\\n\\n\" +\n\t\t\t\t\t\t\"Then use /model to select a model.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate API key (async)\n\t\t\tconst apiKey = await getApiKeyForModel(currentModel);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t`No API key found for ${currentModel.provider}.\\n\\n` +\n\t\t\t\t\t\t`Set the appropriate environment variable or update ~/.pi/agent/models.json`,\n\t\t\t\t);\n\t\t\t\tthis.editor.setText(text);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if agent is currently streaming\n\t\t\tif (this.agent.state.isStreaming) {\n\t\t\t\t// Queue the message instead of submitting\n\t\t\t\tthis.queuedMessages.push(text);\n\n\t\t\t\t// Queue in agent\n\t\t\t\tawait this.agent.queueMessage({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t});\n\n\t\t\t\t// Update pending messages display\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\n\t\t\t\t// Clear editor\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// All good, proceed with submission\n\t\t\tif (this.onInputCallback) {\n\t\t\t\tthis.onInputCallback(text);\n\t\t\t}\n\t\t};\n\n\t\t// Start the UI\n\t\tthis.ui.start();\n\t\tthis.isInitialized = true;\n\n\t\t// Subscribe to agent events for UI updates and session saving\n\t\tthis.subscribeToAgent();\n\n\t\t// Set up theme file watcher for live reload\n\t\tonThemeChange(() => {\n\t\t\tthis.ui.invalidate();\n\t\t\tthis.updateEditorBorderColor();\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\t// Set up git branch watcher\n\t\tthis.footer.watchBranch(() => {\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate subscribeToAgent(): void {\n\t\tthis.unsubscribe = this.agent.subscribe(async (event) => {\n\t\t\t// Handle UI updates\n\t\t\tawait this.handleEvent(event, this.agent.state);\n\n\t\t\t// Save messages to session\n\t\t\tif (event.type === \"message_end\") {\n\t\t\t\tthis.sessionManager.saveMessage(event.message);\n\n\t\t\t\t// Check if we should initialize session now (after first user+assistant exchange)\n\t\t\t\tif (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {\n\t\t\t\t\tthis.sessionManager.startSession(this.agent.state);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {\n\t\tif (!this.isInitialized) {\n\t\t\tawait this.init();\n\t\t}\n\n\t\t// Update footer with current stats\n\t\tthis.footer.updateState(state);\n\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\t// Show loading animation\n\t\t\t\t// Note: Don't disable submit - we handle queuing in onSubmit callback\n\t\t\t\t// Stop old loader before clearing\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.stop();\n\t\t\t\t}\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.loadingAnimation = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t\"Working... (esc to interrupt)\",\n\t\t\t\t);\n\t\t\t\tthis.statusContainer.addChild(this.loadingAnimation);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_start\":\n\t\t\t\tif (event.message.role === \"user\") {\n\t\t\t\t\t// Check if this is a queued message\n\t\t\t\t\tconst userMsg = event.message as any;\n\t\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\t\tconst messageText = textBlocks.map((c: any) => c.text).join(\"\");\n\n\t\t\t\t\tconst queuedIndex = this.queuedMessages.indexOf(messageText);\n\t\t\t\t\tif (queuedIndex !== -1) {\n\t\t\t\t\t\t// Remove from queued messages\n\t\t\t\t\t\tthis.queuedMessages.splice(queuedIndex, 1);\n\t\t\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Show user message immediately and clear editor\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"assistant\") {\n\t\t\t\t\t// Create assistant component for streaming\n\t\t\t\t\tthis.streamingComponent = new AssistantMessageComponent();\n\t\t\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent.updateContent(event.message as AssistantMessage);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_update\":\n\t\t\t\t// Update streaming component\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\t\t\t\t\tthis.streamingComponent.updateContent(assistantMsg);\n\n\t\t\t\t\t// Create tool execution components as soon as we see tool calls\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\t\t// Only create if we haven't created it yet\n\t\t\t\t\t\t\tif (!this.pendingTools.has(content.id)) {\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(\"\", 0, 0));\n\t\t\t\t\t\t\t\tconst component = new ToolExecutionComponent(content.name, content.arguments);\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Update existing component with latest arguments as they stream\n\t\t\t\t\t\t\t\tconst component = this.pendingTools.get(content.id);\n\t\t\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\t\t\tcomponent.updateArgs(content.arguments);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_end\":\n\t\t\t\t// Skip user messages (already shown in message_start)\n\t\t\t\tif (event.message.role === \"user\") {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\n\t\t\t\t\t// Update streaming component with final message (includes stopReason)\n\t\t\t\t\tthis.streamingComponent.updateContent(assistantMsg);\n\n\t\t\t\t\t// If message was aborted or errored, mark all pending tool components as failed\n\t\t\t\t\tif (assistantMsg.stopReason === \"aborted\" || assistantMsg.stopReason === \"error\") {\n\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\tassistantMsg.stopReason === \"aborted\" ? \"Operation aborted\" : assistantMsg.errorMessage || \"Error\";\n\t\t\t\t\t\tfor (const [toolCallId, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.pendingTools.clear();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Keep the streaming component - it's now the final assistant message\n\t\t\t\t\tthis.streamingComponent = null;\n\n\t\t\t\t\t// Invalidate footer cache to refresh git branch (in case agent executed git commands)\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t// Component should already exist from message_update, but create if missing\n\t\t\t\tif (!this.pendingTools.has(event.toolCallId)) {\n\t\t\t\t\tconst component = new ToolExecutionComponent(event.toolName, event.args);\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\tthis.pendingTools.set(event.toolCallId, component);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Update the existing tool component with the result\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Convert result to the format expected by updateResult\n\t\t\t\t\tconst resultData =\n\t\t\t\t\t\ttypeof event.result === \"string\"\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: event.result }],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\tisError: event.isError,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\tcontent: event.result.content,\n\t\t\t\t\t\t\t\t\tdetails: event.result.details,\n\t\t\t\t\t\t\t\t\tisError: event.isError,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\tcomponent.updateResult(resultData);\n\t\t\t\t\tthis.pendingTools.delete(event.toolCallId);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\":\n\t\t\t\t// Stop loading animation\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.stop();\n\t\t\t\t\tthis.loadingAnimation = null;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent = null;\n\t\t\t\t}\n\t\t\t\tthis.pendingTools.clear();\n\t\t\t\t// Note: Don't need to re-enable submit - we never disable it\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate addMessageToChat(message: Message): void {\n\t\tif (message.role === \"user\") {\n\t\t\tconst userMsg = message as any;\n\t\t\t// Extract text content from content blocks\n\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\tif (textContent) {\n\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);\n\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\tthis.isFirstUserMessage = false;\n\t\t\t}\n\t\t} else if (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\n\t\t\t// Add assistant message component\n\t\t\tconst assistantComponent = new AssistantMessageComponent(assistantMsg);\n\t\t\tthis.chatContainer.addChild(assistantComponent);\n\t\t}\n\t\t// Note: tool calls and results are now handled via tool_execution_start/end events\n\t}\n\n\trenderInitialMessages(state: AgentState): void {\n\t\t// Render all existing messages (for --continue mode)\n\t\t// Reset first user message flag for initial render\n\t\tthis.isFirstUserMessage = true;\n\n\t\t// Update footer with loaded state\n\t\tthis.footer.updateState(state);\n\n\t\t// Update editor border color based on current thinking level\n\t\tthis.updateEditorBorderColor();\n\n\t\t// Render messages\n\t\tfor (let i = 0; i < state.messages.length; i++) {\n\t\t\tconst message = state.messages[i];\n\n\t\t\tif (message.role === \"user\") {\n\t\t\t\tconst userMsg = message as any;\n\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\t\tif (textContent) {\n\t\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);\n\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\tthis.isFirstUserMessage = false;\n\t\t\t\t}\n\t\t\t} else if (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\tconst assistantComponent = new AssistantMessageComponent(assistantMsg);\n\t\t\t\tthis.chatContainer.addChild(assistantComponent);\n\n\t\t\t\t// Create tool execution components for any tool calls\n\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\tconst component = new ToolExecutionComponent(content.name, content.arguments);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\n\t\t\t\t\t\t// If message was aborted/errored, immediately mark tool as failed\n\t\t\t\t\t\tif (assistantMsg.stopReason === \"aborted\" || assistantMsg.stopReason === \"error\") {\n\t\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\t\tassistantMsg.stopReason === \"aborted\"\n\t\t\t\t\t\t\t\t\t? \"Operation aborted\"\n\t\t\t\t\t\t\t\t\t: assistantMsg.errorMessage || \"Error\";\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Store in map so we can update with results later\n\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.role === \"toolResult\") {\n\t\t\t\t// Update existing tool execution component with results\t\t\t\t;\n\t\t\t\tconst component = this.pendingTools.get(message.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\tcontent: message.content,\n\t\t\t\t\t\tdetails: message.details,\n\t\t\t\t\t\tisError: message.isError,\n\t\t\t\t\t});\n\t\t\t\t\t// Remove from pending map since it's complete\n\t\t\t\t\tthis.pendingTools.delete(message.toolCallId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Clear pending tools after rendering initial messages\n\t\tthis.pendingTools.clear();\n\t\tthis.ui.requestRender();\n\t}\n\n\tasync getUserInput(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onInputCallback = (text: string) => {\n\t\t\t\tthis.onInputCallback = undefined;\n\t\t\t\tresolve(text);\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\t// Handle Ctrl+C double-press logic\n\t\tconst now = Date.now();\n\t\tconst timeSinceLastCtrlC = now - this.lastSigintTime;\n\n\t\tif (timeSinceLastCtrlC < 500) {\n\t\t\t// Second Ctrl+C within 500ms - exit\n\t\t\tthis.stop();\n\t\t\tprocess.exit(0);\n\t\t} else {\n\t\t\t// First Ctrl+C - clear the editor\n\t\t\tthis.clearEditor();\n\t\t\tthis.lastSigintTime = now;\n\t\t}\n\t}\n\n\tprivate updateEditorBorderColor(): void {\n\t\tconst level = this.agent.state.thinkingLevel || \"off\";\n\t\tthis.editor.borderColor = theme.getThinkingBorderColor(level);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate cycleThinkingLevel(): void {\n\t\t// Only cycle if model supports thinking\n\t\tif (!this.agent.state.model?.reasoning) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Current model does not support thinking\"), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst levels: ThinkingLevel[] = [\"off\", \"minimal\", \"low\", \"medium\", \"high\"];\n\t\tconst currentLevel = this.agent.state.thinkingLevel || \"off\";\n\t\tconst currentIndex = levels.indexOf(currentLevel);\n\t\tconst nextIndex = (currentIndex + 1) % levels.length;\n\t\tconst nextLevel = levels[nextIndex];\n\n\t\t// Apply the new thinking level\n\t\tthis.agent.setThinkingLevel(nextLevel);\n\n\t\t// Save thinking level change to session and settings\n\t\tthis.sessionManager.saveThinkingLevelChange(nextLevel);\n\t\tthis.settingsManager.setDefaultThinkingLevel(nextLevel);\n\n\t\t// Update border color\n\t\tthis.updateEditorBorderColor();\n\n\t\t// Show brief notification\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Thinking level: ${nextLevel}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async cycleModel(): Promise<void> {\n\t\t// Use scoped models if available, otherwise all available models\n\t\tif (this.scopedModels.length > 0) {\n\t\t\t// Use scoped models with thinking levels\n\t\t\tif (this.scopedModels.length === 1) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Only one model in scope\"), 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst currentModel = this.agent.state.model;\n\t\t\tlet currentIndex = this.scopedModels.findIndex(\n\t\t\t\t(sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider,\n\t\t\t);\n\n\t\t\t// If current model not in scope, start from first\n\t\t\tif (currentIndex === -1) {\n\t\t\t\tcurrentIndex = 0;\n\t\t\t}\n\n\t\t\tconst nextIndex = (currentIndex + 1) % this.scopedModels.length;\n\t\t\tconst nextEntry = this.scopedModels[nextIndex];\n\t\t\tconst nextModel = nextEntry.model;\n\t\t\tconst nextThinking = nextEntry.thinkingLevel;\n\n\t\t\t// Validate API key\n\t\t\tconst apiKey = await getApiKeyForModel(nextModel);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Switch model\n\t\t\tthis.agent.setModel(nextModel);\n\n\t\t\t// Save model change to session and settings\n\t\t\tthis.sessionManager.saveModelChange(nextModel.provider, nextModel.id);\n\t\t\tthis.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);\n\n\t\t\t// Apply thinking level (silently use \"off\" if model doesn't support thinking)\n\t\t\tconst effectiveThinking = nextModel.reasoning ? nextThinking : \"off\";\n\t\t\tthis.agent.setThinkingLevel(effectiveThinking);\n\t\t\tthis.sessionManager.saveThinkingLevelChange(effectiveThinking);\n\t\t\tthis.settingsManager.setDefaultThinkingLevel(effectiveThinking);\n\t\t\tthis.updateEditorBorderColor();\n\n\t\t\t// Show notification\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tconst thinkingStr = nextModel.reasoning && nextThinking !== \"off\" ? ` (thinking: ${nextThinking})` : \"\";\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"dim\", `Switched to ${nextModel.name || nextModel.id}${thinkingStr}`), 1, 0),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t} else {\n\t\t\t// Fallback to all available models (no thinking level changes)\n\t\t\tconst { models: availableModels, error } = await getAvailableModels();\n\t\t\tif (error) {\n\t\t\t\tthis.showError(`Failed to load models: ${error}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (availableModels.length === 0) {\n\t\t\t\tthis.showError(\"No models available to cycle\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (availableModels.length === 1) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Only one model available\"), 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst currentModel = this.agent.state.model;\n\t\t\tlet currentIndex = availableModels.findIndex(\n\t\t\t\t(m) => m.id === currentModel?.id && m.provider === currentModel?.provider,\n\t\t\t);\n\n\t\t\t// If current model not in scope, start from first\n\t\t\tif (currentIndex === -1) {\n\t\t\t\tcurrentIndex = 0;\n\t\t\t}\n\n\t\t\tconst nextIndex = (currentIndex + 1) % availableModels.length;\n\t\t\tconst nextModel = availableModels[nextIndex];\n\n\t\t\t// Validate API key\n\t\t\tconst apiKey = await getApiKeyForModel(nextModel);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Switch model\n\t\t\tthis.agent.setModel(nextModel);\n\n\t\t\t// Save model change to session and settings\n\t\t\tthis.sessionManager.saveModelChange(nextModel.provider, nextModel.id);\n\t\t\tthis.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);\n\n\t\t\t// Show notification\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate toggleToolOutputExpansion(): void {\n\t\tthis.toolOutputExpanded = !this.toolOutputExpanded;\n\n\t\t// Update all tool execution components\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (child instanceof ToolExecutionComponent) {\n\t\t\t\tchild.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tclearEditor(): void {\n\t\tthis.editor.setText(\"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowError(errorMessage: string): void {\n\t\t// Show error message in the chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowWarning(warningMessage: string): void {\n\t\t// Show warning message in the chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"warning\", `Warning: ${warningMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showThinkingSelector(): void {\n\t\t// Create thinking selector with current level\n\t\tthis.thinkingSelector = new ThinkingSelectorComponent(\n\t\t\tthis.agent.state.thinkingLevel,\n\t\t\t(level) => {\n\t\t\t\t// Apply the selected thinking level\n\t\t\t\tthis.agent.setThinkingLevel(level);\n\n\t\t\t\t// Save thinking level change to session and settings\n\t\t\t\tthis.sessionManager.saveThinkingLevelChange(level);\n\t\t\t\tthis.settingsManager.setDefaultThinkingLevel(level);\n\n\t\t\t\t// Update border color\n\t\t\t\tthis.updateEditorBorderColor();\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Thinking level: ${level}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideThinkingSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideThinkingSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.thinkingSelector);\n\t\tthis.ui.setFocus(this.thinkingSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideThinkingSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.thinkingSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showQueueModeSelector(): void {\n\t\t// Create queue mode selector with current mode\n\t\tthis.queueModeSelector = new QueueModeSelectorComponent(\n\t\t\tthis.agent.getQueueMode(),\n\t\t\t(mode) => {\n\t\t\t\t// Apply the selected queue mode\n\t\t\t\tthis.agent.setQueueMode(mode);\n\n\t\t\t\t// Save queue mode to settings\n\t\t\t\tthis.settingsManager.setQueueMode(mode);\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Queue mode: ${mode}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideQueueModeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideQueueModeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.queueModeSelector);\n\t\tthis.ui.setFocus(this.queueModeSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideQueueModeSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.queueModeSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showThemeSelector(): void {\n\t\t// Get current theme from settings\n\t\tconst currentTheme = this.settingsManager.getTheme() || \"dark\";\n\n\t\t// Create theme selector\n\t\tthis.themeSelector = new ThemeSelectorComponent(\n\t\t\tcurrentTheme,\n\t\t\t(themeName) => {\n\t\t\t\t// Apply the selected theme\n\t\t\t\tconst result = setTheme(themeName);\n\n\t\t\t\t// Save theme to settings\n\t\t\t\tthis.settingsManager.setTheme(themeName);\n\n\t\t\t\t// Invalidate all components to clear cached rendering\n\t\t\t\tthis.ui.invalidate();\n\n\t\t\t\t// Show confirmation or error message\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tif (result.success) {\n\t\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Theme: ${themeName}`), 1, 0);\n\t\t\t\t\tthis.chatContainer.addChild(confirmText);\n\t\t\t\t} else {\n\t\t\t\t\tconst errorText = new Text(\n\t\t\t\t\t\ttheme.fg(\"error\", `Failed to load theme \"${themeName}\": ${result.error}\\nFell back to dark theme.`),\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t);\n\t\t\t\t\tthis.chatContainer.addChild(errorText);\n\t\t\t\t}\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideThemeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideThemeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t(themeName) => {\n\t\t\t\t// Preview theme on selection change\n\t\t\t\tconst result = setTheme(themeName);\n\t\t\t\tif (result.success) {\n\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\t// If failed, theme already fell back to dark, just don't re-render\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.themeSelector);\n\t\tthis.ui.setFocus(this.themeSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideThemeSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.themeSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showModelSelector(): void {\n\t\t// Create model selector with current model\n\t\tthis.modelSelector = new ModelSelectorComponent(\n\t\t\tthis.ui,\n\t\t\tthis.agent.state.model,\n\t\t\tthis.settingsManager,\n\t\t\t(model) => {\n\t\t\t\t// Apply the selected model\n\t\t\t\tthis.agent.setModel(model);\n\n\t\t\t\t// Save model change to session\n\t\t\t\tthis.sessionManager.saveModelChange(model.provider, model.id);\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Model: ${model.id}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideModelSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideModelSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.modelSelector);\n\t\tthis.ui.setFocus(this.modelSelector);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideModelSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.modelSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showUserMessageSelector(): void {\n\t\t// Extract all user messages from the current state\n\t\tconst userMessages: Array<{ index: number; text: string }> = [];\n\n\t\tfor (let i = 0; i < this.agent.state.messages.length; i++) {\n\t\t\tconst message = this.agent.state.messages[i];\n\t\t\tif (message.role === \"user\") {\n\t\t\t\tconst userMsg = message as any;\n\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\t\tif (textContent) {\n\t\t\t\t\tuserMessages.push({ index: i, text: textContent });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Don't show selector if there are no messages or only one message\n\t\tif (userMessages.length <= 1) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"No messages to branch from\"), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Create user message selector\n\t\tthis.userMessageSelector = new UserMessageSelectorComponent(\n\t\t\tuserMessages,\n\t\t\t(messageIndex) => {\n\t\t\t\t// Get the selected user message text to put in the editor\n\t\t\t\tconst selectedMessage = this.agent.state.messages[messageIndex];\n\t\t\t\tconst selectedUserMsg = selectedMessage as any;\n\t\t\t\tconst textBlocks = selectedUserMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst selectedText = textBlocks.map((c: any) => c.text).join(\"\");\n\n\t\t\t\t// Create a branched session with messages UP TO (but not including) the selected message\n\t\t\t\tconst newSessionFile = this.sessionManager.createBranchedSession(this.agent.state, messageIndex - 1);\n\n\t\t\t\t// Set the new session file as active\n\t\t\t\tthis.sessionManager.setSessionFile(newSessionFile);\n\n\t\t\t\t// Truncate messages in agent state to before the selected message\n\t\t\t\tconst truncatedMessages = this.agent.state.messages.slice(0, messageIndex);\n\t\t\t\tthis.agent.replaceMessages(truncatedMessages);\n\n\t\t\t\t// Clear and re-render the chat\n\t\t\t\tthis.chatContainer.clear();\n\t\t\t\tthis.isFirstUserMessage = true;\n\t\t\t\tthis.renderInitialMessages(this.agent.state);\n\n\t\t\t\t// Show confirmation message\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"dim\", `Branched to new session from message ${messageIndex}`), 1, 0),\n\t\t\t\t);\n\n\t\t\t\t// Put the selected message in the editor\n\t\t\t\tthis.editor.setText(selectedText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideUserMessageSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideUserMessageSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.userMessageSelector);\n\t\tthis.ui.setFocus(this.userMessageSelector.getMessageList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideUserMessageSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.userMessageSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate async showOAuthSelector(mode: \"login\" | \"logout\"): Promise<void> {\n\t\t// For logout mode, filter to only show logged-in providers\n\t\tlet providersToShow: string[] = [];\n\t\tif (mode === \"logout\") {\n\t\t\tconst loggedInProviders = listOAuthProviders();\n\t\t\tif (loggedInProviders.length === 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"dim\", \"No OAuth providers logged in. Use /login first.\"), 1, 0),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprovidersToShow = loggedInProviders;\n\t\t}\n\n\t\t// Create OAuth selector\n\t\tthis.oauthSelector = new OAuthSelectorComponent(\n\t\t\tmode,\n\t\t\tasync (providerId: any) => {\n\t\t\t\t// Hide selector first\n\t\t\t\tthis.hideOAuthSelector();\n\n\t\t\t\tif (mode === \"login\") {\n\t\t\t\t\t// Handle login\n\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Logging in to ${providerId}...`), 1, 0));\n\t\t\t\t\tthis.ui.requestRender();\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait login(\n\t\t\t\t\t\t\tproviderId,\n\t\t\t\t\t\t\t(url: string) => {\n\t\t\t\t\t\t\t\t// Show auth URL to user\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"accent\", \"Opening browser to:\"), 1, 0));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"accent\", url), 1, 0));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\t\t\tnew Text(theme.fg(\"warning\", \"Paste the authorization code below:\"), 1, 0),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tthis.ui.requestRender();\n\n\t\t\t\t\t\t\t\t// Open URL in browser\n\t\t\t\t\t\t\t\tconst openCmd =\n\t\t\t\t\t\t\t\t\tprocess.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\t\t\t\t\t\t\t\texec(`${openCmd} \"${url}\"`);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tasync () => {\n\t\t\t\t\t\t\t\t// Prompt for code with a simple Input\n\t\t\t\t\t\t\t\treturn new Promise<string>((resolve) => {\n\t\t\t\t\t\t\t\t\tconst codeInput = new Input();\n\t\t\t\t\t\t\t\t\tcodeInput.onSubmit = () => {\n\t\t\t\t\t\t\t\t\t\tconst code = codeInput.getValue();\n\t\t\t\t\t\t\t\t\t\t// Restore editor\n\t\t\t\t\t\t\t\t\t\tthis.editorContainer.clear();\n\t\t\t\t\t\t\t\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\t\t\t\t\t\t\t\tthis.ui.setFocus(this.editor);\n\t\t\t\t\t\t\t\t\t\tresolve(code);\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\t\tthis.editorContainer.clear();\n\t\t\t\t\t\t\t\t\tthis.editorContainer.addChild(codeInput);\n\t\t\t\t\t\t\t\t\tthis.ui.setFocus(codeInput);\n\t\t\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Success - invalidate OAuth cache so footer updates\n\t\t\t\t\t\tinvalidateOAuthCache();\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"success\", `✓ Successfully logged in to ${providerId}`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"dim\", `Tokens saved to ~/.pi/agent/oauth.json`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\tthis.showError(`Login failed: ${error.message}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Handle logout\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait logout(providerId);\n\n\t\t\t\t\t\t// Invalidate OAuth cache so footer updates\n\t\t\t\t\t\tinvalidateOAuthCache();\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"success\", `✓ Successfully logged out of ${providerId}`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"dim\", `Credentials removed from ~/.pi/agent/oauth.json`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\tthis.showError(`Logout failed: ${error.message}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Cancel - just hide the selector\n\t\t\t\tthis.hideOAuthSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.oauthSelector);\n\t\tthis.ui.setFocus(this.oauthSelector);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideOAuthSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.oauthSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate handleExportCommand(text: string): void {\n\t\t// Parse optional filename from command: /export [filename]\n\t\tconst parts = text.split(/\\s+/);\n\t\tconst outputPath = parts.length > 1 ? parts[1] : undefined;\n\n\t\ttry {\n\t\t\t// Export session to HTML\n\t\t\tconst filePath = exportSessionToHtml(this.sessionManager, this.agent.state, outputPath);\n\n\t\t\t// Show success message in chat - matching thinking level style\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session exported to: ${filePath}`), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error: any) {\n\t\t\t// Show error message in chat\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"error\", `Failed to export session: ${error.message || \"Unknown error\"}`), 1, 0),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate handleSessionCommand(): void {\n\t\t// Get session info\n\t\tconst sessionFile = this.sessionManager.getSessionFile();\n\t\tconst state = this.agent.state;\n\n\t\t// Count messages\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\t\tconst totalMessages = state.messages.length;\n\n\t\t// Count tool calls from assistant messages\n\t\tlet toolCalls = 0;\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate cumulative usage from all assistant messages (same as footer)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;\n\n\t\t// Build info text\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${sessionFile}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${this.sessionManager.getSessionId()}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${totalMessages}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Input:\")} ${totalInput.toLocaleString()}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Output:\")} ${totalOutput.toLocaleString()}\\n`;\n\t\tif (totalCacheRead > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Read:\")} ${totalCacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (totalCacheWrite > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Write:\")} ${totalCacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${totalTokens.toLocaleString()}\\n`;\n\n\t\tif (totalCost > 0) {\n\t\t\tinfo += `\\n${theme.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${totalCost.toFixed(4)}`;\n\t\t}\n\n\t\t// Show info in chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleChangelogCommand(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\t// Show all entries in reverse order (oldest first, newest last)\n\t\tconst changelogMarkdown =\n\t\t\tallEntries.length > 0\n\t\t\t\t? allEntries\n\t\t\t\t\t\t.reverse()\n\t\t\t\t\t\t.map((e) => e.content)\n\t\t\t\t\t\t.join(\"\\n\\n\")\n\t\t\t\t: \"No changelog entries found.\";\n\n\t\t// Display in chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleClearCommand(): Promise<void> {\n\t\t// Unsubscribe first to prevent processing abort events\n\t\tthis.unsubscribe?.();\n\n\t\t// Abort and wait for completion\n\t\tthis.agent.abort();\n\t\tawait this.agent.waitForIdle();\n\n\t\t// Stop loading animation\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = null;\n\t\t}\n\t\tthis.statusContainer.clear();\n\n\t\t// Reset agent and session\n\t\tthis.agent.reset();\n\t\tthis.sessionManager.reset();\n\n\t\t// Resubscribe to agent\n\t\tthis.subscribeToAgent();\n\n\t\t// Clear UI state\n\t\tthis.chatContainer.clear();\n\t\tthis.pendingMessagesContainer.clear();\n\t\tthis.queuedMessages = [];\n\t\tthis.streamingComponent = null;\n\t\tthis.pendingTools.clear();\n\t\tthis.isFirstUserMessage = true;\n\n\t\t// Show confirmation\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(\n\t\t\tnew Text(theme.fg(\"accent\", \"✓ Context cleared\") + \"\\n\" + theme.fg(\"muted\", \"Started fresh session\"), 1, 1),\n\t\t);\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDebugCommand(): void {\n\t\t// Force a render and capture all lines with their widths\n\t\tconst width = (this.ui as any).terminal.columns;\n\t\tconst allLines = this.ui.render(width);\n\n\t\tconst debugLogPath = path.join(os.homedir(), \".pi\", \"agent\", \"pi-debug.log\");\n\t\tconst debugData = [\n\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t`Terminal width: ${width}`,\n\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\"\",\n\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t...allLines.map((line, idx) => {\n\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t}),\n\t\t\t\"\",\n\t\t].join(\"\\n\");\n\n\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\t// Show confirmation\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"accent\", \"✓ Debug log written\") + \"\\n\" + theme.fg(\"muted\", `~/.pi/agent/pi-debug.log`),\n\t\t\t\t1,\n\t\t\t\t1,\n\t\t\t),\n\t\t);\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate updatePendingMessagesDisplay(): void {\n\t\tthis.pendingMessagesContainer.clear();\n\n\t\tif (this.queuedMessages.length > 0) {\n\t\t\tthis.pendingMessagesContainer.addChild(new Spacer(1));\n\n\t\t\tfor (const message of this.queuedMessages) {\n\t\t\t\tconst queuedText = theme.fg(\"dim\", \"Queued: \" + message);\n\t\t\t\tthis.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = null;\n\t\t}\n\t\tthis.footer.dispose();\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.stop();\n\t\t\tthis.isInitialized = false;\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"tui-renderer.d.ts","sourceRoot":"","sources":["../../src/tui/tui-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { Agent, AgentEvent, AgentState, ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Message, Model } from \"@mariozechner/pi-ai\";\nimport type { SlashCommand } from \"@mariozechner/pi-tui\";\nimport {\n\tCombinedAutocompleteProvider,\n\tContainer,\n\tInput,\n\tLoader,\n\tMarkdown,\n\tProcessTerminal,\n\tSpacer,\n\tText,\n\tTruncatedText,\n\tTUI,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport { exec } from \"child_process\";\nimport { getChangelogPath, parseChangelog } from \"../changelog.js\";\nimport { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from \"../config.js\";\nimport { exportSessionToHtml } from \"../export-html.js\";\nimport { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from \"../model-config.js\";\nimport { listOAuthProviders, login, logout } from \"../oauth/index.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type { SettingsManager } from \"../settings-manager.js\";\nimport { expandSlashCommand, type FileSlashCommand, loadSlashCommands } from \"../slash-commands.js\";\nimport { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from \"../theme/theme.js\";\nimport { AssistantMessageComponent } from \"./assistant-message.js\";\nimport { CustomEditor } from \"./custom-editor.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\nimport { FooterComponent } from \"./footer.js\";\nimport { ModelSelectorComponent } from \"./model-selector.js\";\nimport { OAuthSelectorComponent } from \"./oauth-selector.js\";\nimport { QueueModeSelectorComponent } from \"./queue-mode-selector.js\";\nimport { ThemeSelectorComponent } from \"./theme-selector.js\";\nimport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nimport { ToolExecutionComponent } from \"./tool-execution.js\";\nimport { UserMessageComponent } from \"./user-message.js\";\nimport { UserMessageSelectorComponent } from \"./user-message-selector.js\";\n\n/**\n * TUI renderer for the coding agent\n */\nexport class TuiRenderer {\n\tprivate ui: TUI;\n\tprivate chatContainer: Container;\n\tprivate pendingMessagesContainer: Container;\n\tprivate statusContainer: Container;\n\tprivate editor: CustomEditor;\n\tprivate editorContainer: Container; // Container to swap between editor and selector\n\tprivate footer: FooterComponent;\n\tprivate agent: Agent;\n\tprivate sessionManager: SessionManager;\n\tprivate settingsManager: SettingsManager;\n\tprivate version: string;\n\tprivate isInitialized = false;\n\tprivate onInputCallback?: (text: string) => void;\n\tprivate loadingAnimation: Loader | null = null;\n\n\tprivate lastSigintTime = 0;\n\tprivate changelogMarkdown: string | null = null;\n\tprivate newVersion: string | null = null;\n\n\t// Message queueing\n\tprivate queuedMessages: string[] = [];\n\n\t// Streaming message tracking\n\tprivate streamingComponent: AssistantMessageComponent | null = null;\n\n\t// Tool execution tracking: toolCallId -> component\n\tprivate pendingTools = new Map<string, ToolExecutionComponent>();\n\n\t// Thinking level selector\n\tprivate thinkingSelector: ThinkingSelectorComponent | null = null;\n\n\t// Queue mode selector\n\tprivate queueModeSelector: QueueModeSelectorComponent | null = null;\n\n\t// Theme selector\n\tprivate themeSelector: ThemeSelectorComponent | null = null;\n\n\t// Model selector\n\tprivate modelSelector: ModelSelectorComponent | null = null;\n\n\t// User message selector (for branching)\n\tprivate userMessageSelector: UserMessageSelectorComponent | null = null;\n\n\t// OAuth selector\n\tprivate oauthSelector: any | null = null;\n\n\t// Track if this is the first user message (to skip spacer)\n\tprivate isFirstUserMessage = true;\n\n\t// Model scope for quick cycling\n\tprivate scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }> = [];\n\n\t// Tool output expansion state\n\tprivate toolOutputExpanded = false;\n\n\t// Agent subscription unsubscribe function\n\tprivate unsubscribe?: () => void;\n\n\t// File-based slash commands\n\tprivate fileCommands: FileSlashCommand[] = [];\n\n\tconstructor(\n\t\tagent: Agent,\n\t\tsessionManager: SessionManager,\n\t\tsettingsManager: SettingsManager,\n\t\tversion: string,\n\t\tchangelogMarkdown: string | null = null,\n\t\tnewVersion: string | null = null,\n\t\tscopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }> = [],\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.version = version;\n\t\tthis.newVersion = newVersion;\n\t\tthis.changelogMarkdown = changelogMarkdown;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.ui = new TUI(new ProcessTerminal());\n\t\tthis.chatContainer = new Container();\n\t\tthis.pendingMessagesContainer = new Container();\n\t\tthis.statusContainer = new Container();\n\t\tthis.editor = new CustomEditor(getEditorTheme());\n\t\tthis.editorContainer = new Container(); // Container to hold editor or selector\n\t\tthis.editorContainer.addChild(this.editor); // Start with editor\n\t\tthis.footer = new FooterComponent(agent.state);\n\n\t\t// Define slash commands\n\t\tconst thinkingCommand: SlashCommand = {\n\t\t\tname: \"thinking\",\n\t\t\tdescription: \"Select reasoning level (opens selector UI)\",\n\t\t};\n\n\t\tconst modelCommand: SlashCommand = {\n\t\t\tname: \"model\",\n\t\t\tdescription: \"Select model (opens selector UI)\",\n\t\t};\n\n\t\tconst exportCommand: SlashCommand = {\n\t\t\tname: \"export\",\n\t\t\tdescription: \"Export session to HTML file\",\n\t\t};\n\n\t\tconst sessionCommand: SlashCommand = {\n\t\t\tname: \"session\",\n\t\t\tdescription: \"Show session info and stats\",\n\t\t};\n\n\t\tconst changelogCommand: SlashCommand = {\n\t\t\tname: \"changelog\",\n\t\t\tdescription: \"Show changelog entries\",\n\t\t};\n\n\t\tconst branchCommand: SlashCommand = {\n\t\t\tname: \"branch\",\n\t\t\tdescription: \"Create a new branch from a previous message\",\n\t\t};\n\n\t\tconst loginCommand: SlashCommand = {\n\t\t\tname: \"login\",\n\t\t\tdescription: \"Login with OAuth provider\",\n\t\t};\n\n\t\tconst logoutCommand: SlashCommand = {\n\t\t\tname: \"logout\",\n\t\t\tdescription: \"Logout from OAuth provider\",\n\t\t};\n\n\t\tconst queueCommand: SlashCommand = {\n\t\t\tname: \"queue\",\n\t\t\tdescription: \"Select message queue mode (opens selector UI)\",\n\t\t};\n\n\t\tconst themeCommand: SlashCommand = {\n\t\t\tname: \"theme\",\n\t\t\tdescription: \"Select color theme (opens selector UI)\",\n\t\t};\n\n\t\tconst clearCommand: SlashCommand = {\n\t\t\tname: \"clear\",\n\t\t\tdescription: \"Clear context and start a fresh session\",\n\t\t};\n\n\t\t// Load file-based slash commands\n\t\tthis.fileCommands = loadSlashCommands();\n\n\t\t// Convert file commands to SlashCommand format\n\t\tconst fileSlashCommands: SlashCommand[] = this.fileCommands.map((cmd) => ({\n\t\t\tname: cmd.name,\n\t\t\tdescription: cmd.description,\n\t\t}));\n\n\t\t// Setup autocomplete for file paths and slash commands\n\t\tconst autocompleteProvider = new CombinedAutocompleteProvider(\n\t\t\t[\n\t\t\t\tthinkingCommand,\n\t\t\t\tmodelCommand,\n\t\t\t\tthemeCommand,\n\t\t\t\texportCommand,\n\t\t\t\tsessionCommand,\n\t\t\t\tchangelogCommand,\n\t\t\t\tbranchCommand,\n\t\t\t\tloginCommand,\n\t\t\t\tlogoutCommand,\n\t\t\t\tqueueCommand,\n\t\t\t\tclearCommand,\n\t\t\t\t...fileSlashCommands,\n\t\t\t],\n\t\t\tprocess.cwd(),\n\t\t\tfdPath,\n\t\t);\n\t\tthis.editor.setAutocompleteProvider(autocompleteProvider);\n\t}\n\n\tasync init(): Promise<void> {\n\t\tif (this.isInitialized) return;\n\n\t\t// Add header with logo and instructions\n\t\tconst logo = theme.bold(theme.fg(\"accent\", APP_NAME)) + theme.fg(\"dim\", ` v${this.version}`);\n\t\tconst instructions =\n\t\t\ttheme.fg(\"dim\", \"esc\") +\n\t\t\ttheme.fg(\"muted\", \" to interrupt\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+c\") +\n\t\t\ttheme.fg(\"muted\", \" to clear\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+c twice\") +\n\t\t\ttheme.fg(\"muted\", \" to exit\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+k\") +\n\t\t\ttheme.fg(\"muted\", \" to delete line\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"shift+tab\") +\n\t\t\ttheme.fg(\"muted\", \" to cycle thinking\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+p\") +\n\t\t\ttheme.fg(\"muted\", \" to cycle models\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"ctrl+o\") +\n\t\t\ttheme.fg(\"muted\", \" to expand tools\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"/\") +\n\t\t\ttheme.fg(\"muted\", \" for commands\") +\n\t\t\t\"\\n\" +\n\t\t\ttheme.fg(\"dim\", \"drop files\") +\n\t\t\ttheme.fg(\"muted\", \" to attach\");\n\t\tconst header = new Text(logo + \"\\n\" + instructions, 1, 0);\n\n\t\t// Setup UI layout\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(header);\n\t\tthis.ui.addChild(new Spacer(1));\n\n\t\t// Add new version notification if available\n\t\tif (this.newVersion) {\n\t\t\tthis.ui.addChild(new DynamicBorder((text) => theme.fg(\"warning\", text)));\n\t\t\tthis.ui.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.bold(theme.fg(\"warning\", \"Update Available\")) +\n\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\ttheme.fg(\"muted\", `New version ${this.newVersion} is available. Run: `) +\n\t\t\t\t\t\ttheme.fg(\"accent\", \"npm install -g @mariozechner/pi-coding-agent\"),\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ui.addChild(new DynamicBorder((text) => theme.fg(\"warning\", text)));\n\t\t}\n\n\t\t// Add changelog if provided\n\t\tif (this.changelogMarkdown) {\n\t\t\tthis.ui.addChild(new DynamicBorder());\n\t\t\tthis.ui.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\t\tthis.ui.addChild(new Spacer(1));\n\t\t\tthis.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));\n\t\t\tthis.ui.addChild(new Spacer(1));\n\t\t\tthis.ui.addChild(new DynamicBorder());\n\t\t}\n\n\t\tthis.ui.addChild(this.chatContainer);\n\t\tthis.ui.addChild(this.pendingMessagesContainer);\n\t\tthis.ui.addChild(this.statusContainer);\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(this.editorContainer); // Use container that can hold editor or selector\n\t\tthis.ui.addChild(this.footer);\n\t\tthis.ui.setFocus(this.editor);\n\n\t\t// Set up custom key handlers on the editor\n\t\tthis.editor.onEscape = () => {\n\t\t\t// Intercept Escape key when processing\n\t\t\tif (this.loadingAnimation) {\n\t\t\t\t// Get all queued messages\n\t\t\t\tconst queuedText = this.queuedMessages.join(\"\\n\\n\");\n\n\t\t\t\t// Get current editor text\n\t\t\t\tconst currentText = this.editor.getText();\n\n\t\t\t\t// Combine: queued messages + current editor text\n\t\t\t\tconst combinedText = [queuedText, currentText].filter((t) => t.trim()).join(\"\\n\\n\");\n\n\t\t\t\t// Put back in editor\n\t\t\t\tthis.editor.setText(combinedText);\n\n\t\t\t\t// Clear queued messages\n\t\t\t\tthis.queuedMessages = [];\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\n\t\t\t\t// Clear agent's queue too\n\t\t\t\tthis.agent.clearMessageQueue();\n\n\t\t\t\t// Abort\n\t\t\t\tthis.agent.abort();\n\t\t\t}\n\t\t};\n\n\t\tthis.editor.onCtrlC = () => {\n\t\t\tthis.handleCtrlC();\n\t\t};\n\n\t\tthis.editor.onShiftTab = () => {\n\t\t\tthis.cycleThinkingLevel();\n\t\t};\n\n\t\tthis.editor.onCtrlP = () => {\n\t\t\tthis.cycleModel();\n\t\t};\n\n\t\tthis.editor.onCtrlO = () => {\n\t\t\tthis.toggleToolOutputExpansion();\n\t\t};\n\n\t\t// Handle editor submission\n\t\tthis.editor.onSubmit = async (text: string) => {\n\t\t\ttext = text.trim();\n\t\t\tif (!text) return;\n\n\t\t\t// Check for /thinking command\n\t\t\tif (text === \"/thinking\") {\n\t\t\t\t// Show thinking level selector\n\t\t\t\tthis.showThinkingSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /model command\n\t\t\tif (text === \"/model\") {\n\t\t\t\t// Show model selector\n\t\t\t\tthis.showModelSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /export command\n\t\t\tif (text.startsWith(\"/export\")) {\n\t\t\t\tthis.handleExportCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /session command\n\t\t\tif (text === \"/session\") {\n\t\t\t\tthis.handleSessionCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /changelog command\n\t\t\tif (text === \"/changelog\") {\n\t\t\t\tthis.handleChangelogCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /branch command\n\t\t\tif (text === \"/branch\") {\n\t\t\t\tthis.showUserMessageSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /login command\n\t\t\tif (text === \"/login\") {\n\t\t\t\tthis.showOAuthSelector(\"login\");\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /logout command\n\t\t\tif (text === \"/logout\") {\n\t\t\t\tthis.showOAuthSelector(\"logout\");\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /queue command\n\t\t\tif (text === \"/queue\") {\n\t\t\t\tthis.showQueueModeSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /theme command\n\t\t\tif (text === \"/theme\") {\n\t\t\t\tthis.showThemeSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /clear command\n\t\t\tif (text === \"/clear\") {\n\t\t\t\tthis.handleClearCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /debug command\n\t\t\tif (text === \"/debug\") {\n\t\t\t\tthis.handleDebugCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for file-based slash commands\n\t\t\ttext = expandSlashCommand(text, this.fileCommands);\n\n\t\t\t// Normal message submission - validate model and API key first\n\t\t\tconst currentModel = this.agent.state.model;\n\t\t\tif (!currentModel) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t\"No model selected.\\n\\n\" +\n\t\t\t\t\t\t\"Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\\n\" +\n\t\t\t\t\t\t`or create ${getModelsPath()}\\n\\n` +\n\t\t\t\t\t\t\"Then use /model to select a model.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate API key (async)\n\t\t\tconst apiKey = await getApiKeyForModel(currentModel);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis.showError(\n\t\t\t\t\t`No API key found for ${currentModel.provider}.\\n\\n` +\n\t\t\t\t\t\t`Set the appropriate environment variable or update ${getModelsPath()}`,\n\t\t\t\t);\n\t\t\t\tthis.editor.setText(text);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if agent is currently streaming\n\t\t\tif (this.agent.state.isStreaming) {\n\t\t\t\t// Queue the message instead of submitting\n\t\t\t\tthis.queuedMessages.push(text);\n\n\t\t\t\t// Queue in agent\n\t\t\t\tawait this.agent.queueMessage({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t});\n\n\t\t\t\t// Update pending messages display\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\n\t\t\t\t// Clear editor\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// All good, proceed with submission\n\t\t\tif (this.onInputCallback) {\n\t\t\t\tthis.onInputCallback(text);\n\t\t\t}\n\t\t};\n\n\t\t// Start the UI\n\t\tthis.ui.start();\n\t\tthis.isInitialized = true;\n\n\t\t// Subscribe to agent events for UI updates and session saving\n\t\tthis.subscribeToAgent();\n\n\t\t// Set up theme file watcher for live reload\n\t\tonThemeChange(() => {\n\t\t\tthis.ui.invalidate();\n\t\t\tthis.updateEditorBorderColor();\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\t// Set up git branch watcher\n\t\tthis.footer.watchBranch(() => {\n\t\t\tthis.ui.requestRender();\n\t\t});\n\t}\n\n\tprivate subscribeToAgent(): void {\n\t\tthis.unsubscribe = this.agent.subscribe(async (event) => {\n\t\t\t// Handle UI updates\n\t\t\tawait this.handleEvent(event, this.agent.state);\n\n\t\t\t// Save messages to session\n\t\t\tif (event.type === \"message_end\") {\n\t\t\t\tthis.sessionManager.saveMessage(event.message);\n\n\t\t\t\t// Check if we should initialize session now (after first user+assistant exchange)\n\t\t\t\tif (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {\n\t\t\t\t\tthis.sessionManager.startSession(this.agent.state);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {\n\t\tif (!this.isInitialized) {\n\t\t\tawait this.init();\n\t\t}\n\n\t\t// Update footer with current stats\n\t\tthis.footer.updateState(state);\n\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\t// Show loading animation\n\t\t\t\t// Note: Don't disable submit - we handle queuing in onSubmit callback\n\t\t\t\t// Stop old loader before clearing\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.stop();\n\t\t\t\t}\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.loadingAnimation = new Loader(\n\t\t\t\t\tthis.ui,\n\t\t\t\t\t(spinner) => theme.fg(\"accent\", spinner),\n\t\t\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\t\t\t\"Working... (esc to interrupt)\",\n\t\t\t\t);\n\t\t\t\tthis.statusContainer.addChild(this.loadingAnimation);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_start\":\n\t\t\t\tif (event.message.role === \"user\") {\n\t\t\t\t\t// Check if this is a queued message\n\t\t\t\t\tconst userMsg = event.message as any;\n\t\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\t\tconst messageText = textBlocks.map((c: any) => c.text).join(\"\");\n\n\t\t\t\t\tconst queuedIndex = this.queuedMessages.indexOf(messageText);\n\t\t\t\t\tif (queuedIndex !== -1) {\n\t\t\t\t\t\t// Remove from queued messages\n\t\t\t\t\t\tthis.queuedMessages.splice(queuedIndex, 1);\n\t\t\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Show user message immediately and clear editor\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"assistant\") {\n\t\t\t\t\t// Create assistant component for streaming\n\t\t\t\t\tthis.streamingComponent = new AssistantMessageComponent();\n\t\t\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent.updateContent(event.message as AssistantMessage);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_update\":\n\t\t\t\t// Update streaming component\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\t\t\t\t\tthis.streamingComponent.updateContent(assistantMsg);\n\n\t\t\t\t\t// Create tool execution components as soon as we see tool calls\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\t\t// Only create if we haven't created it yet\n\t\t\t\t\t\t\tif (!this.pendingTools.has(content.id)) {\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(\"\", 0, 0));\n\t\t\t\t\t\t\t\tconst component = new ToolExecutionComponent(content.name, content.arguments);\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Update existing component with latest arguments as they stream\n\t\t\t\t\t\t\t\tconst component = this.pendingTools.get(content.id);\n\t\t\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\t\t\tcomponent.updateArgs(content.arguments);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_end\":\n\t\t\t\t// Skip user messages (already shown in message_start)\n\t\t\t\tif (event.message.role === \"user\") {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\n\t\t\t\t\t// Update streaming component with final message (includes stopReason)\n\t\t\t\t\tthis.streamingComponent.updateContent(assistantMsg);\n\n\t\t\t\t\t// If message was aborted or errored, mark all pending tool components as failed\n\t\t\t\t\tif (assistantMsg.stopReason === \"aborted\" || assistantMsg.stopReason === \"error\") {\n\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\tassistantMsg.stopReason === \"aborted\" ? \"Operation aborted\" : assistantMsg.errorMessage || \"Error\";\n\t\t\t\t\t\tfor (const [toolCallId, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.pendingTools.clear();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Keep the streaming component - it's now the final assistant message\n\t\t\t\t\tthis.streamingComponent = null;\n\n\t\t\t\t\t// Invalidate footer cache to refresh git branch (in case agent executed git commands)\n\t\t\t\t\tthis.footer.invalidate();\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t// Component should already exist from message_update, but create if missing\n\t\t\t\tif (!this.pendingTools.has(event.toolCallId)) {\n\t\t\t\t\tconst component = new ToolExecutionComponent(event.toolName, event.args);\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\tthis.pendingTools.set(event.toolCallId, component);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Update the existing tool component with the result\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Convert result to the format expected by updateResult\n\t\t\t\t\tconst resultData =\n\t\t\t\t\t\ttypeof event.result === \"string\"\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: event.result }],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\tisError: event.isError,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\tcontent: event.result.content,\n\t\t\t\t\t\t\t\t\tdetails: event.result.details,\n\t\t\t\t\t\t\t\t\tisError: event.isError,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\tcomponent.updateResult(resultData);\n\t\t\t\t\tthis.pendingTools.delete(event.toolCallId);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\":\n\t\t\t\t// Stop loading animation\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.stop();\n\t\t\t\t\tthis.loadingAnimation = null;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent = null;\n\t\t\t\t}\n\t\t\t\tthis.pendingTools.clear();\n\t\t\t\t// Note: Don't need to re-enable submit - we never disable it\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate addMessageToChat(message: Message): void {\n\t\tif (message.role === \"user\") {\n\t\t\tconst userMsg = message as any;\n\t\t\t// Extract text content from content blocks\n\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\tif (textContent) {\n\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);\n\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\tthis.isFirstUserMessage = false;\n\t\t\t}\n\t\t} else if (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\n\t\t\t// Add assistant message component\n\t\t\tconst assistantComponent = new AssistantMessageComponent(assistantMsg);\n\t\t\tthis.chatContainer.addChild(assistantComponent);\n\t\t}\n\t\t// Note: tool calls and results are now handled via tool_execution_start/end events\n\t}\n\n\trenderInitialMessages(state: AgentState): void {\n\t\t// Render all existing messages (for --continue mode)\n\t\t// Reset first user message flag for initial render\n\t\tthis.isFirstUserMessage = true;\n\n\t\t// Update footer with loaded state\n\t\tthis.footer.updateState(state);\n\n\t\t// Update editor border color based on current thinking level\n\t\tthis.updateEditorBorderColor();\n\n\t\t// Render messages\n\t\tfor (let i = 0; i < state.messages.length; i++) {\n\t\t\tconst message = state.messages[i];\n\n\t\t\tif (message.role === \"user\") {\n\t\t\t\tconst userMsg = message as any;\n\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\t\tif (textContent) {\n\t\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);\n\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\tthis.isFirstUserMessage = false;\n\t\t\t\t}\n\t\t\t} else if (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\tconst assistantComponent = new AssistantMessageComponent(assistantMsg);\n\t\t\t\tthis.chatContainer.addChild(assistantComponent);\n\n\t\t\t\t// Create tool execution components for any tool calls\n\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\tconst component = new ToolExecutionComponent(content.name, content.arguments);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\n\t\t\t\t\t\t// If message was aborted/errored, immediately mark tool as failed\n\t\t\t\t\t\tif (assistantMsg.stopReason === \"aborted\" || assistantMsg.stopReason === \"error\") {\n\t\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\t\tassistantMsg.stopReason === \"aborted\"\n\t\t\t\t\t\t\t\t\t? \"Operation aborted\"\n\t\t\t\t\t\t\t\t\t: assistantMsg.errorMessage || \"Error\";\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Store in map so we can update with results later\n\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.role === \"toolResult\") {\n\t\t\t\t// Update existing tool execution component with results\t\t\t\t;\n\t\t\t\tconst component = this.pendingTools.get(message.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\tcontent: message.content,\n\t\t\t\t\t\tdetails: message.details,\n\t\t\t\t\t\tisError: message.isError,\n\t\t\t\t\t});\n\t\t\t\t\t// Remove from pending map since it's complete\n\t\t\t\t\tthis.pendingTools.delete(message.toolCallId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Clear pending tools after rendering initial messages\n\t\tthis.pendingTools.clear();\n\t\tthis.ui.requestRender();\n\t}\n\n\tasync getUserInput(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onInputCallback = (text: string) => {\n\t\t\t\tthis.onInputCallback = undefined;\n\t\t\t\tresolve(text);\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\t// Handle Ctrl+C double-press logic\n\t\tconst now = Date.now();\n\t\tconst timeSinceLastCtrlC = now - this.lastSigintTime;\n\n\t\tif (timeSinceLastCtrlC < 500) {\n\t\t\t// Second Ctrl+C within 500ms - exit\n\t\t\tthis.stop();\n\t\t\tprocess.exit(0);\n\t\t} else {\n\t\t\t// First Ctrl+C - clear the editor\n\t\t\tthis.clearEditor();\n\t\t\tthis.lastSigintTime = now;\n\t\t}\n\t}\n\n\tprivate updateEditorBorderColor(): void {\n\t\tconst level = this.agent.state.thinkingLevel || \"off\";\n\t\tthis.editor.borderColor = theme.getThinkingBorderColor(level);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate cycleThinkingLevel(): void {\n\t\t// Only cycle if model supports thinking\n\t\tif (!this.agent.state.model?.reasoning) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Current model does not support thinking\"), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst levels: ThinkingLevel[] = [\"off\", \"minimal\", \"low\", \"medium\", \"high\"];\n\t\tconst currentLevel = this.agent.state.thinkingLevel || \"off\";\n\t\tconst currentIndex = levels.indexOf(currentLevel);\n\t\tconst nextIndex = (currentIndex + 1) % levels.length;\n\t\tconst nextLevel = levels[nextIndex];\n\n\t\t// Apply the new thinking level\n\t\tthis.agent.setThinkingLevel(nextLevel);\n\n\t\t// Save thinking level change to session and settings\n\t\tthis.sessionManager.saveThinkingLevelChange(nextLevel);\n\t\tthis.settingsManager.setDefaultThinkingLevel(nextLevel);\n\n\t\t// Update border color\n\t\tthis.updateEditorBorderColor();\n\n\t\t// Show brief notification\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Thinking level: ${nextLevel}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async cycleModel(): Promise<void> {\n\t\t// Use scoped models if available, otherwise all available models\n\t\tif (this.scopedModels.length > 0) {\n\t\t\t// Use scoped models with thinking levels\n\t\t\tif (this.scopedModels.length === 1) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Only one model in scope\"), 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst currentModel = this.agent.state.model;\n\t\t\tlet currentIndex = this.scopedModels.findIndex(\n\t\t\t\t(sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider,\n\t\t\t);\n\n\t\t\t// If current model not in scope, start from first\n\t\t\tif (currentIndex === -1) {\n\t\t\t\tcurrentIndex = 0;\n\t\t\t}\n\n\t\t\tconst nextIndex = (currentIndex + 1) % this.scopedModels.length;\n\t\t\tconst nextEntry = this.scopedModels[nextIndex];\n\t\t\tconst nextModel = nextEntry.model;\n\t\t\tconst nextThinking = nextEntry.thinkingLevel;\n\n\t\t\t// Validate API key\n\t\t\tconst apiKey = await getApiKeyForModel(nextModel);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Switch model\n\t\t\tthis.agent.setModel(nextModel);\n\n\t\t\t// Save model change to session and settings\n\t\t\tthis.sessionManager.saveModelChange(nextModel.provider, nextModel.id);\n\t\t\tthis.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);\n\n\t\t\t// Apply thinking level (silently use \"off\" if model doesn't support thinking)\n\t\t\tconst effectiveThinking = nextModel.reasoning ? nextThinking : \"off\";\n\t\t\tthis.agent.setThinkingLevel(effectiveThinking);\n\t\t\tthis.sessionManager.saveThinkingLevelChange(effectiveThinking);\n\t\t\tthis.settingsManager.setDefaultThinkingLevel(effectiveThinking);\n\t\t\tthis.updateEditorBorderColor();\n\n\t\t\t// Show notification\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tconst thinkingStr = nextModel.reasoning && nextThinking !== \"off\" ? ` (thinking: ${nextThinking})` : \"\";\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"dim\", `Switched to ${nextModel.name || nextModel.id}${thinkingStr}`), 1, 0),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t} else {\n\t\t\t// Fallback to all available models (no thinking level changes)\n\t\t\tconst { models: availableModels, error } = await getAvailableModels();\n\t\t\tif (error) {\n\t\t\t\tthis.showError(`Failed to load models: ${error}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (availableModels.length === 0) {\n\t\t\t\tthis.showError(\"No models available to cycle\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (availableModels.length === 1) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"Only one model available\"), 1, 0));\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst currentModel = this.agent.state.model;\n\t\t\tlet currentIndex = availableModels.findIndex(\n\t\t\t\t(m) => m.id === currentModel?.id && m.provider === currentModel?.provider,\n\t\t\t);\n\n\t\t\t// If current model not in scope, start from first\n\t\t\tif (currentIndex === -1) {\n\t\t\t\tcurrentIndex = 0;\n\t\t\t}\n\n\t\t\tconst nextIndex = (currentIndex + 1) % availableModels.length;\n\t\t\tconst nextModel = availableModels[nextIndex];\n\n\t\t\t// Validate API key\n\t\t\tconst apiKey = await getApiKeyForModel(nextModel);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Switch model\n\t\t\tthis.agent.setModel(nextModel);\n\n\t\t\t// Save model change to session and settings\n\t\t\tthis.sessionManager.saveModelChange(nextModel.provider, nextModel.id);\n\t\t\tthis.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);\n\n\t\t\t// Show notification\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate toggleToolOutputExpansion(): void {\n\t\tthis.toolOutputExpanded = !this.toolOutputExpanded;\n\n\t\t// Update all tool execution components\n\t\tfor (const child of this.chatContainer.children) {\n\t\t\tif (child instanceof ToolExecutionComponent) {\n\t\t\t\tchild.setExpanded(this.toolOutputExpanded);\n\t\t\t}\n\t\t}\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tclearEditor(): void {\n\t\tthis.editor.setText(\"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowError(errorMessage: string): void {\n\t\t// Show error message in the chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowWarning(warningMessage: string): void {\n\t\t// Show warning message in the chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(theme.fg(\"warning\", `Warning: ${warningMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showThinkingSelector(): void {\n\t\t// Create thinking selector with current level\n\t\tthis.thinkingSelector = new ThinkingSelectorComponent(\n\t\t\tthis.agent.state.thinkingLevel,\n\t\t\t(level) => {\n\t\t\t\t// Apply the selected thinking level\n\t\t\t\tthis.agent.setThinkingLevel(level);\n\n\t\t\t\t// Save thinking level change to session and settings\n\t\t\t\tthis.sessionManager.saveThinkingLevelChange(level);\n\t\t\t\tthis.settingsManager.setDefaultThinkingLevel(level);\n\n\t\t\t\t// Update border color\n\t\t\t\tthis.updateEditorBorderColor();\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Thinking level: ${level}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideThinkingSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideThinkingSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.thinkingSelector);\n\t\tthis.ui.setFocus(this.thinkingSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideThinkingSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.thinkingSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showQueueModeSelector(): void {\n\t\t// Create queue mode selector with current mode\n\t\tthis.queueModeSelector = new QueueModeSelectorComponent(\n\t\t\tthis.agent.getQueueMode(),\n\t\t\t(mode) => {\n\t\t\t\t// Apply the selected queue mode\n\t\t\t\tthis.agent.setQueueMode(mode);\n\n\t\t\t\t// Save queue mode to settings\n\t\t\t\tthis.settingsManager.setQueueMode(mode);\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Queue mode: ${mode}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideQueueModeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideQueueModeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.queueModeSelector);\n\t\tthis.ui.setFocus(this.queueModeSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideQueueModeSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.queueModeSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showThemeSelector(): void {\n\t\t// Get current theme from settings\n\t\tconst currentTheme = this.settingsManager.getTheme() || \"dark\";\n\n\t\t// Create theme selector\n\t\tthis.themeSelector = new ThemeSelectorComponent(\n\t\t\tcurrentTheme,\n\t\t\t(themeName) => {\n\t\t\t\t// Apply the selected theme\n\t\t\t\tconst result = setTheme(themeName);\n\n\t\t\t\t// Save theme to settings\n\t\t\t\tthis.settingsManager.setTheme(themeName);\n\n\t\t\t\t// Invalidate all components to clear cached rendering\n\t\t\t\tthis.ui.invalidate();\n\n\t\t\t\t// Show confirmation or error message\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tif (result.success) {\n\t\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Theme: ${themeName}`), 1, 0);\n\t\t\t\t\tthis.chatContainer.addChild(confirmText);\n\t\t\t\t} else {\n\t\t\t\t\tconst errorText = new Text(\n\t\t\t\t\t\ttheme.fg(\"error\", `Failed to load theme \"${themeName}\": ${result.error}\\nFell back to dark theme.`),\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t);\n\t\t\t\t\tthis.chatContainer.addChild(errorText);\n\t\t\t\t}\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideThemeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideThemeSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t(themeName) => {\n\t\t\t\t// Preview theme on selection change\n\t\t\t\tconst result = setTheme(themeName);\n\t\t\t\tif (result.success) {\n\t\t\t\t\tthis.ui.invalidate();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\t// If failed, theme already fell back to dark, just don't re-render\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.themeSelector);\n\t\tthis.ui.setFocus(this.themeSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideThemeSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.themeSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showModelSelector(): void {\n\t\t// Create model selector with current model\n\t\tthis.modelSelector = new ModelSelectorComponent(\n\t\t\tthis.ui,\n\t\t\tthis.agent.state.model,\n\t\t\tthis.settingsManager,\n\t\t\t(model) => {\n\t\t\t\t// Apply the selected model\n\t\t\t\tthis.agent.setModel(model);\n\n\t\t\t\t// Save model change to session\n\t\t\t\tthis.sessionManager.saveModelChange(model.provider, model.id);\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(theme.fg(\"dim\", `Model: ${model.id}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideModelSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideModelSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.modelSelector);\n\t\tthis.ui.setFocus(this.modelSelector);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideModelSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.modelSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showUserMessageSelector(): void {\n\t\t// Extract all user messages from the current state\n\t\tconst userMessages: Array<{ index: number; text: string }> = [];\n\n\t\tfor (let i = 0; i < this.agent.state.messages.length; i++) {\n\t\t\tconst message = this.agent.state.messages[i];\n\t\t\tif (message.role === \"user\") {\n\t\t\t\tconst userMsg = message as any;\n\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\t\tif (textContent) {\n\t\t\t\t\tuserMessages.push({ index: i, text: textContent });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Don't show selector if there are no messages or only one message\n\t\tif (userMessages.length <= 1) {\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", \"No messages to branch from\"), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\t// Create user message selector\n\t\tthis.userMessageSelector = new UserMessageSelectorComponent(\n\t\t\tuserMessages,\n\t\t\t(messageIndex) => {\n\t\t\t\t// Get the selected user message text to put in the editor\n\t\t\t\tconst selectedMessage = this.agent.state.messages[messageIndex];\n\t\t\t\tconst selectedUserMsg = selectedMessage as any;\n\t\t\t\tconst textBlocks = selectedUserMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst selectedText = textBlocks.map((c: any) => c.text).join(\"\");\n\n\t\t\t\t// Create a branched session with messages UP TO (but not including) the selected message\n\t\t\t\tconst newSessionFile = this.sessionManager.createBranchedSession(this.agent.state, messageIndex - 1);\n\n\t\t\t\t// Set the new session file as active\n\t\t\t\tthis.sessionManager.setSessionFile(newSessionFile);\n\n\t\t\t\t// Truncate messages in agent state to before the selected message\n\t\t\t\tconst truncatedMessages = this.agent.state.messages.slice(0, messageIndex);\n\t\t\t\tthis.agent.replaceMessages(truncatedMessages);\n\n\t\t\t\t// Clear and re-render the chat\n\t\t\t\tthis.chatContainer.clear();\n\t\t\t\tthis.isFirstUserMessage = true;\n\t\t\t\tthis.renderInitialMessages(this.agent.state);\n\n\t\t\t\t// Show confirmation message\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"dim\", `Branched to new session from message ${messageIndex}`), 1, 0),\n\t\t\t\t);\n\n\t\t\t\t// Put the selected message in the editor\n\t\t\t\tthis.editor.setText(selectedText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideUserMessageSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideUserMessageSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.userMessageSelector);\n\t\tthis.ui.setFocus(this.userMessageSelector.getMessageList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideUserMessageSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.userMessageSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate async showOAuthSelector(mode: \"login\" | \"logout\"): Promise<void> {\n\t\t// For logout mode, filter to only show logged-in providers\n\t\tlet providersToShow: string[] = [];\n\t\tif (mode === \"logout\") {\n\t\t\tconst loggedInProviders = listOAuthProviders();\n\t\t\tif (loggedInProviders.length === 0) {\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"dim\", \"No OAuth providers logged in. Use /login first.\"), 1, 0),\n\t\t\t\t);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprovidersToShow = loggedInProviders;\n\t\t}\n\n\t\t// Create OAuth selector\n\t\tthis.oauthSelector = new OAuthSelectorComponent(\n\t\t\tmode,\n\t\t\tasync (providerId: any) => {\n\t\t\t\t// Hide selector first\n\t\t\t\tthis.hideOAuthSelector();\n\n\t\t\t\tif (mode === \"login\") {\n\t\t\t\t\t// Handle login\n\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Logging in to ${providerId}...`), 1, 0));\n\t\t\t\t\tthis.ui.requestRender();\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait login(\n\t\t\t\t\t\t\tproviderId,\n\t\t\t\t\t\t\t(url: string) => {\n\t\t\t\t\t\t\t\t// Show auth URL to user\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"accent\", \"Opening browser to:\"), 1, 0));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"accent\", url), 1, 0));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\t\t\tnew Text(theme.fg(\"warning\", \"Paste the authorization code below:\"), 1, 0),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tthis.ui.requestRender();\n\n\t\t\t\t\t\t\t\t// Open URL in browser\n\t\t\t\t\t\t\t\tconst openCmd =\n\t\t\t\t\t\t\t\t\tprocess.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\t\t\t\t\t\t\t\texec(`${openCmd} \"${url}\"`);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tasync () => {\n\t\t\t\t\t\t\t\t// Prompt for code with a simple Input\n\t\t\t\t\t\t\t\treturn new Promise<string>((resolve) => {\n\t\t\t\t\t\t\t\t\tconst codeInput = new Input();\n\t\t\t\t\t\t\t\t\tcodeInput.onSubmit = () => {\n\t\t\t\t\t\t\t\t\t\tconst code = codeInput.getValue();\n\t\t\t\t\t\t\t\t\t\t// Restore editor\n\t\t\t\t\t\t\t\t\t\tthis.editorContainer.clear();\n\t\t\t\t\t\t\t\t\t\tthis.editorContainer.addChild(this.editor);\n\t\t\t\t\t\t\t\t\t\tthis.ui.setFocus(this.editor);\n\t\t\t\t\t\t\t\t\t\tresolve(code);\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\t\tthis.editorContainer.clear();\n\t\t\t\t\t\t\t\t\tthis.editorContainer.addChild(codeInput);\n\t\t\t\t\t\t\t\t\tthis.ui.setFocus(codeInput);\n\t\t\t\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Success - invalidate OAuth cache so footer updates\n\t\t\t\t\t\tinvalidateOAuthCache();\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"success\", `✓ Successfully logged in to ${providerId}`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Tokens saved to ${getOAuthPath()}`), 1, 0));\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\tthis.showError(`Login failed: ${error.message}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Handle logout\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait logout(providerId);\n\n\t\t\t\t\t\t// Invalidate OAuth cache so footer updates\n\t\t\t\t\t\tinvalidateOAuthCache();\n\t\t\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"success\", `✓ Successfully logged out of ${providerId}`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.chatContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"dim\", `Credentials removed from ${getOAuthPath()}`), 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\tthis.showError(`Logout failed: ${error.message}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Cancel - just hide the selector\n\t\t\t\tthis.hideOAuthSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.oauthSelector);\n\t\tthis.ui.setFocus(this.oauthSelector);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideOAuthSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.oauthSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate handleExportCommand(text: string): void {\n\t\t// Parse optional filename from command: /export [filename]\n\t\tconst parts = text.split(/\\s+/);\n\t\tconst outputPath = parts.length > 1 ? parts[1] : undefined;\n\n\t\ttry {\n\t\t\t// Export session to HTML\n\t\t\tconst filePath = exportSessionToHtml(this.sessionManager, this.agent.state, outputPath);\n\n\t\t\t// Show success message in chat - matching thinking level style\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(theme.fg(\"dim\", `Session exported to: ${filePath}`), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error: any) {\n\t\t\t// Show error message in chat\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"error\", `Failed to export session: ${error.message || \"Unknown error\"}`), 1, 0),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate handleSessionCommand(): void {\n\t\t// Get session info\n\t\tconst sessionFile = this.sessionManager.getSessionFile();\n\t\tconst state = this.agent.state;\n\n\t\t// Count messages\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\t\tconst totalMessages = state.messages.length;\n\n\t\t// Count tool calls from assistant messages\n\t\tlet toolCalls = 0;\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate cumulative usage from all assistant messages (same as footer)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;\n\n\t\t// Build info text\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${sessionFile}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${this.sessionManager.getSessionId()}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${totalMessages}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Input:\")} ${totalInput.toLocaleString()}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Output:\")} ${totalOutput.toLocaleString()}\\n`;\n\t\tif (totalCacheRead > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Read:\")} ${totalCacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (totalCacheWrite > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Write:\")} ${totalCacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${totalTokens.toLocaleString()}\\n`;\n\n\t\tif (totalCost > 0) {\n\t\t\tinfo += `\\n${theme.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${totalCost.toFixed(4)}`;\n\t\t}\n\n\t\t// Show info in chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleChangelogCommand(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\t// Show all entries in reverse order (oldest first, newest last)\n\t\tconst changelogMarkdown =\n\t\t\tallEntries.length > 0\n\t\t\t\t? allEntries\n\t\t\t\t\t\t.reverse()\n\t\t\t\t\t\t.map((e) => e.content)\n\t\t\t\t\t\t.join(\"\\n\\n\")\n\t\t\t\t: \"No changelog entries found.\";\n\n\t\t// Display in chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));\n\t\tthis.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async handleClearCommand(): Promise<void> {\n\t\t// Unsubscribe first to prevent processing abort events\n\t\tthis.unsubscribe?.();\n\n\t\t// Abort and wait for completion\n\t\tthis.agent.abort();\n\t\tawait this.agent.waitForIdle();\n\n\t\t// Stop loading animation\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = null;\n\t\t}\n\t\tthis.statusContainer.clear();\n\n\t\t// Reset agent and session\n\t\tthis.agent.reset();\n\t\tthis.sessionManager.reset();\n\n\t\t// Resubscribe to agent\n\t\tthis.subscribeToAgent();\n\n\t\t// Clear UI state\n\t\tthis.chatContainer.clear();\n\t\tthis.pendingMessagesContainer.clear();\n\t\tthis.queuedMessages = [];\n\t\tthis.streamingComponent = null;\n\t\tthis.pendingTools.clear();\n\t\tthis.isFirstUserMessage = true;\n\n\t\t// Show confirmation\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(\n\t\t\tnew Text(theme.fg(\"accent\", \"✓ Context cleared\") + \"\\n\" + theme.fg(\"muted\", \"Started fresh session\"), 1, 1),\n\t\t);\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate handleDebugCommand(): void {\n\t\t// Force a render and capture all lines with their widths\n\t\tconst width = (this.ui as any).terminal.columns;\n\t\tconst allLines = this.ui.render(width);\n\n\t\tconst debugLogPath = getDebugLogPath();\n\t\tconst debugData = [\n\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t`Terminal width: ${width}`,\n\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\"\",\n\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t...allLines.map((line, idx) => {\n\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t}),\n\t\t\t\"\",\n\t\t].join(\"\\n\");\n\n\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\t// Show confirmation\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(\n\t\t\tnew Text(theme.fg(\"accent\", \"✓ Debug log written\") + \"\\n\" + theme.fg(\"muted\", debugLogPath), 1, 1),\n\t\t);\n\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate updatePendingMessagesDisplay(): void {\n\t\tthis.pendingMessagesContainer.clear();\n\n\t\tif (this.queuedMessages.length > 0) {\n\t\t\tthis.pendingMessagesContainer.addChild(new Spacer(1));\n\n\t\t\tfor (const message of this.queuedMessages) {\n\t\t\t\tconst queuedText = theme.fg(\"dim\", \"Queued: \" + message);\n\t\t\t\tthis.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = null;\n\t\t}\n\t\tthis.footer.dispose();\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.stop();\n\t\t\tthis.isInitialized = false;\n\t\t}\n\t}\n}\n"]}
@@ -1,9 +1,9 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
3
  import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
5
4
  import { exec } from "child_process";
6
5
  import { getChangelogPath, parseChangelog } from "../changelog.js";
6
+ import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
7
7
  import { exportSessionToHtml } from "../export-html.js";
8
8
  import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js";
9
9
  import { listOAuthProviders, login, logout } from "../oauth/index.js";
@@ -159,7 +159,7 @@ export class TuiRenderer {
159
159
  if (this.isInitialized)
160
160
  return;
161
161
  // Add header with logo and instructions
162
- const logo = theme.bold(theme.fg("accent", "pi")) + theme.fg("dim", ` v${this.version}`);
162
+ const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
163
163
  const instructions = theme.fg("dim", "esc") +
164
164
  theme.fg("muted", " to interrupt") +
165
165
  "\n" +
@@ -335,7 +335,7 @@ export class TuiRenderer {
335
335
  if (!currentModel) {
336
336
  this.showError("No model selected.\n\n" +
337
337
  "Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n" +
338
- "or create ~/.pi/agent/models.json\n\n" +
338
+ `or create ${getModelsPath()}\n\n` +
339
339
  "Then use /model to select a model.");
340
340
  return;
341
341
  }
@@ -343,7 +343,7 @@ export class TuiRenderer {
343
343
  const apiKey = await getApiKeyForModel(currentModel);
344
344
  if (!apiKey) {
345
345
  this.showError(`No API key found for ${currentModel.provider}.\n\n` +
346
- `Set the appropriate environment variable or update ~/.pi/agent/models.json`);
346
+ `Set the appropriate environment variable or update ${getModelsPath()}`);
347
347
  this.editor.setText(text);
348
348
  return;
349
349
  }
@@ -1065,7 +1065,7 @@ export class TuiRenderer {
1065
1065
  invalidateOAuthCache();
1066
1066
  this.chatContainer.addChild(new Spacer(1));
1067
1067
  this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0));
1068
- this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ~/.pi/agent/oauth.json`), 1, 0));
1068
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0));
1069
1069
  this.ui.requestRender();
1070
1070
  }
1071
1071
  catch (error) {
@@ -1080,7 +1080,7 @@ export class TuiRenderer {
1080
1080
  invalidateOAuthCache();
1081
1081
  this.chatContainer.addChild(new Spacer(1));
1082
1082
  this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0));
1083
- this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ~/.pi/agent/oauth.json`), 1, 0));
1083
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0));
1084
1084
  this.ui.requestRender();
1085
1085
  }
1086
1086
  catch (error) {
@@ -1239,7 +1239,7 @@ export class TuiRenderer {
1239
1239
  // Force a render and capture all lines with their widths
1240
1240
  const width = this.ui.terminal.columns;
1241
1241
  const allLines = this.ui.render(width);
1242
- const debugLogPath = path.join(os.homedir(), ".pi", "agent", "pi-debug.log");
1242
+ const debugLogPath = getDebugLogPath();
1243
1243
  const debugData = [
1244
1244
  `Debug output at ${new Date().toISOString()}`,
1245
1245
  `Terminal width: ${width}`,
@@ -1257,7 +1257,7 @@ export class TuiRenderer {
1257
1257
  fs.writeFileSync(debugLogPath, debugData);
1258
1258
  // Show confirmation
1259
1259
  this.chatContainer.addChild(new Spacer(1));
1260
- this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", `~/.pi/agent/pi-debug.log`), 1, 1));
1260
+ this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
1261
1261
  this.ui.requestRender();
1262
1262
  }
1263
1263
  updatePendingMessagesDisplay() {