@mariozechner/pi-coding-agent 0.11.2 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## [0.11.2] - 2025-12-01
3
+ ## [0.11.3] - 2025-12-01
4
+
5
+ ### Added
6
+
7
+ - **Circular Menu Navigation**: All menus (model selector, message history, file picker) now wrap around when navigating past the first or last item. Pressing up at the top jumps to the bottom, and pressing down at the bottom jumps to the top. ([#82](https://github.com/badlogic/pi-mono/pull/82) by [@butelo](https://github.com/butelo))
4
8
 
5
9
  ### Fixed
6
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../src/tui/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAgB,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAU9D;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,GAAG,CAAM;IAEjB,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,eAAe,EAAE,eAAe,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EA+CpB;YAEa,UAAU;IAgCxB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,UAAU;IAqDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA2BjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../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\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredModels.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../src/tui/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAgB,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAU9D;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,GAAG,CAAM;IAEjB,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,eAAe,EAAE,eAAe,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EA+CpB;YAEa,UAAU;IAgCxB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,UAAU;IAqDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA2BjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport { Container, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getAvailableModels } from \"../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"]}
@@ -145,14 +145,14 @@ export class ModelSelectorComponent extends Container {
145
145
  }
146
146
  }
147
147
  handleInput(keyData) {
148
- // Up arrow
148
+ // Up arrow - wrap to bottom when at top
149
149
  if (keyData === "\x1b[A") {
150
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
150
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
151
151
  this.updateList();
152
152
  }
153
- // Down arrow
153
+ // Down arrow - wrap to top when at bottom
154
154
  else if (keyData === "\x1b[B") {
155
- this.selectedIndex = Math.min(this.filteredModels.length - 1, this.selectedIndex + 1);
155
+ this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
156
156
  this.updateList();
157
157
  }
158
158
  // Enter
@@ -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\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredModels.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst 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":"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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 +1 @@
1
- {"version":3,"file":"user-message-selector.d.ts","sourceRoot":"","sources":["../../src/tui/user-message-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAI/E,UAAU,eAAe;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,cAAM,eAAgB,YAAW,SAAS;IACzC,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,aAAa,CAAa;IAC3B,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,OAAO,CAAC,UAAU,CAAc;IAEhC,YAAY,QAAQ,EAAE,eAAe,EAAE,EAKtC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8C9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA4BjC;CACD;AAED;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,SAAS;IAC1D,OAAO,CAAC,WAAW,CAAkB;IAErC,YAAY,QAAQ,EAAE,eAAe,EAAE,EAAE,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA0BtG;IAED,cAAc,IAAI,eAAe,CAEhC;CACD","sourcesContent":["import { type Component, Container, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface UserMessageItem {\n\tindex: number; // Index in the full messages array\n\ttext: string; // The message text\n\ttimestamp?: string; // Optional timestamp if available\n}\n\n/**\n * Custom user message list component with selection\n */\nclass UserMessageList implements Component {\n\tprivate messages: UserMessageItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tpublic onSelect?: (messageIndex: number) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 10; // Max messages visible\n\n\tconstructor(messages: UserMessageItem[]) {\n\t\t// Store messages in chronological order (oldest to newest)\n\t\tthis.messages = messages;\n\t\t// Start with the last (most recent) message selected\n\t\tthis.selectedIndex = Math.max(0, messages.length - 1);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.messages.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", \" No user messages found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);\n\n\t\t// Render visible messages (2 lines per message + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst message = this.messages[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Normalize message to single line\n\t\t\tconst normalizedMessage = message.text.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor\n\t\t\tconst truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);\n\t\t\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\tlines.push(messageLine);\n\n\t\t\t// Second line: metadata (position in history)\n\t\t\tconst position = i + 1;\n\t\t\tconst metadata = ` Message ${position} of ${this.messages.length}`;\n\t\t\tconst metadataLine = theme.fg(\"muted\", metadata);\n\t\t\tlines.push(metadataLine);\n\t\t\tlines.push(\"\"); // Blank line between messages\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.messages.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.messages.length})`);\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - go to previous (older) message\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow - go to next (newer) message\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.messages.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter - select message and branch\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selected = this.messages[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.index);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - cancel\n\t\telse if (keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Component that renders a user message selector for branching\n */\nexport class UserMessageSelectorComponent extends Container {\n\tprivate messageList: UserMessageList;\n\n\tconstructor(messages: UserMessageItem[], onSelect: (messageIndex: number) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Branch from Message\"), 1, 0));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", \"Select a message to create a new branch from that point\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create message list\n\t\tthis.messageList = new UserMessageList(messages);\n\t\tthis.messageList.onSelect = onSelect;\n\t\tthis.messageList.onCancel = onCancel;\n\n\t\tthis.addChild(this.messageList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Auto-cancel if no messages or only one message\n\t\tif (messages.length <= 1) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetMessageList(): UserMessageList {\n\t\treturn this.messageList;\n\t}\n}\n"]}
1
+ {"version":3,"file":"user-message-selector.d.ts","sourceRoot":"","sources":["../../src/tui/user-message-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAI/E,UAAU,eAAe;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,cAAM,eAAgB,YAAW,SAAS;IACzC,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,aAAa,CAAa;IAC3B,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,OAAO,CAAC,UAAU,CAAc;IAEhC,YAAY,QAAQ,EAAE,eAAe,EAAE,EAKtC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8C9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA4BjC;CACD;AAED;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,SAAS;IAC1D,OAAO,CAAC,WAAW,CAAkB;IAErC,YAAY,QAAQ,EAAE,eAAe,EAAE,EAAE,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA0BtG;IAED,cAAc,IAAI,eAAe,CAEhC;CACD","sourcesContent":["import { type Component, Container, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface UserMessageItem {\n\tindex: number; // Index in the full messages array\n\ttext: string; // The message text\n\ttimestamp?: string; // Optional timestamp if available\n}\n\n/**\n * Custom user message list component with selection\n */\nclass UserMessageList implements Component {\n\tprivate messages: UserMessageItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tpublic onSelect?: (messageIndex: number) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 10; // Max messages visible\n\n\tconstructor(messages: UserMessageItem[]) {\n\t\t// Store messages in chronological order (oldest to newest)\n\t\tthis.messages = messages;\n\t\t// Start with the last (most recent) message selected\n\t\tthis.selectedIndex = Math.max(0, messages.length - 1);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.messages.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", \" No user messages found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);\n\n\t\t// Render visible messages (2 lines per message + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst message = this.messages[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Normalize message to single line\n\t\t\tconst normalizedMessage = message.text.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor\n\t\t\tconst truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);\n\t\t\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\tlines.push(messageLine);\n\n\t\t\t// Second line: metadata (position in history)\n\t\t\tconst position = i + 1;\n\t\t\tconst metadata = ` Message ${position} of ${this.messages.length}`;\n\t\t\tconst metadataLine = theme.fg(\"muted\", metadata);\n\t\t\tlines.push(metadataLine);\n\t\t\tlines.push(\"\"); // Blank line between messages\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.messages.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.messages.length})`);\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - go to previous (older) message, wrap to bottom when at top\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1;\n\t\t}\n\t\t// Down arrow - go to next (newer) message, wrap to top when at bottom\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t}\n\t\t// Enter - select message and branch\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selected = this.messages[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.index);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - cancel\n\t\telse if (keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Component that renders a user message selector for branching\n */\nexport class UserMessageSelectorComponent extends Container {\n\tprivate messageList: UserMessageList;\n\n\tconstructor(messages: UserMessageItem[], onSelect: (messageIndex: number) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Branch from Message\"), 1, 0));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", \"Select a message to create a new branch from that point\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create message list\n\t\tthis.messageList = new UserMessageList(messages);\n\t\tthis.messageList.onSelect = onSelect;\n\t\tthis.messageList.onCancel = onCancel;\n\n\t\tthis.addChild(this.messageList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Auto-cancel if no messages or only one message\n\t\tif (messages.length <= 1) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetMessageList(): UserMessageList {\n\t\treturn this.messageList;\n\t}\n}\n"]}
@@ -55,13 +55,13 @@ class UserMessageList {
55
55
  return lines;
56
56
  }
57
57
  handleInput(keyData) {
58
- // Up arrow - go to previous (older) message
58
+ // Up arrow - go to previous (older) message, wrap to bottom when at top
59
59
  if (keyData === "\x1b[A") {
60
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
60
+ this.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1;
61
61
  }
62
- // Down arrow - go to next (newer) message
62
+ // Down arrow - go to next (newer) message, wrap to top when at bottom
63
63
  else if (keyData === "\x1b[B") {
64
- this.selectedIndex = Math.min(this.messages.length - 1, this.selectedIndex + 1);
64
+ this.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;
65
65
  }
66
66
  // Enter - select message and branch
67
67
  else if (keyData === "\r") {
@@ -1 +1 @@
1
- {"version":3,"file":"user-message-selector.js","sourceRoot":"","sources":["../../src/tui/user-message-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, Container, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface UserMessageItem {\n\tindex: number; // Index in the full messages array\n\ttext: string; // The message text\n\ttimestamp?: string; // Optional timestamp if available\n}\n\n/**\n * Custom user message list component with selection\n */\nclass UserMessageList implements Component {\n\tprivate messages: UserMessageItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tpublic onSelect?: (messageIndex: number) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 10; // Max messages visible\n\n\tconstructor(messages: UserMessageItem[]) {\n\t\t// Store messages in chronological order (oldest to newest)\n\t\tthis.messages = messages;\n\t\t// Start with the last (most recent) message selected\n\t\tthis.selectedIndex = Math.max(0, messages.length - 1);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.messages.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", \" No user messages found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);\n\n\t\t// Render visible messages (2 lines per message + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst message = this.messages[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Normalize message to single line\n\t\t\tconst normalizedMessage = message.text.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor\n\t\t\tconst truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);\n\t\t\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\tlines.push(messageLine);\n\n\t\t\t// Second line: metadata (position in history)\n\t\t\tconst position = i + 1;\n\t\t\tconst metadata = ` Message ${position} of ${this.messages.length}`;\n\t\t\tconst metadataLine = theme.fg(\"muted\", metadata);\n\t\t\tlines.push(metadataLine);\n\t\t\tlines.push(\"\"); // Blank line between messages\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.messages.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.messages.length})`);\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - go to previous (older) message\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow - go to next (newer) message\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.messages.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter - select message and branch\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selected = this.messages[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.index);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - cancel\n\t\telse if (keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Component that renders a user message selector for branching\n */\nexport class UserMessageSelectorComponent extends Container {\n\tprivate messageList: UserMessageList;\n\n\tconstructor(messages: UserMessageItem[], onSelect: (messageIndex: number) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Branch from Message\"), 1, 0));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", \"Select a message to create a new branch from that point\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create message list\n\t\tthis.messageList = new UserMessageList(messages);\n\t\tthis.messageList.onSelect = onSelect;\n\t\tthis.messageList.onCancel = onCancel;\n\n\t\tthis.addChild(this.messageList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Auto-cancel if no messages or only one message\n\t\tif (messages.length <= 1) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetMessageList(): UserMessageList {\n\t\treturn this.messageList;\n\t}\n}\n"]}
1
+ {"version":3,"file":"user-message-selector.js","sourceRoot":"","sources":["../../src/tui/user-message-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, Container, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface UserMessageItem {\n\tindex: number; // Index in the full messages array\n\ttext: string; // The message text\n\ttimestamp?: string; // Optional timestamp if available\n}\n\n/**\n * Custom user message list component with selection\n */\nclass UserMessageList implements Component {\n\tprivate messages: UserMessageItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tpublic onSelect?: (messageIndex: number) => void;\n\tpublic onCancel?: () => void;\n\tprivate maxVisible: number = 10; // Max messages visible\n\n\tconstructor(messages: UserMessageItem[]) {\n\t\t// Store messages in chronological order (oldest to newest)\n\t\tthis.messages = messages;\n\t\t// Start with the last (most recent) message selected\n\t\tthis.selectedIndex = Math.max(0, messages.length - 1);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.messages.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", \" No user messages found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);\n\n\t\t// Render visible messages (2 lines per message + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst message = this.messages[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Normalize message to single line\n\t\t\tconst normalizedMessage = message.text.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor\n\t\t\tconst truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);\n\t\t\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\tlines.push(messageLine);\n\n\t\t\t// Second line: metadata (position in history)\n\t\t\tconst position = i + 1;\n\t\t\tconst metadata = ` Message ${position} of ${this.messages.length}`;\n\t\t\tconst metadataLine = theme.fg(\"muted\", metadata);\n\t\t\tlines.push(metadataLine);\n\t\t\tlines.push(\"\"); // Blank line between messages\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.messages.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.messages.length})`);\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - go to previous (older) message, wrap to bottom when at top\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1;\n\t\t}\n\t\t// Down arrow - go to next (newer) message, wrap to top when at bottom\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t}\n\t\t// Enter - select message and branch\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selected = this.messages[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.index);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Ctrl+C - cancel\n\t\telse if (keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Component that renders a user message selector for branching\n */\nexport class UserMessageSelectorComponent extends Container {\n\tprivate messageList: UserMessageList;\n\n\tconstructor(messages: UserMessageItem[], onSelect: (messageIndex: number) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.bold(\"Branch from Message\"), 1, 0));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", \"Select a message to create a new branch from that point\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create message list\n\t\tthis.messageList = new UserMessageList(messages);\n\t\tthis.messageList.onSelect = onSelect;\n\t\tthis.messageList.onCancel = onCancel;\n\n\t\tthis.addChild(this.messageList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Auto-cancel if no messages or only one message\n\t\tif (messages.length <= 1) {\n\t\t\tsetTimeout(() => onCancel(), 100);\n\t\t}\n\t}\n\n\tgetMessageList(): UserMessageList {\n\t\treturn this.messageList;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,9 +22,9 @@
22
22
  "prepublishOnly": "npm run clean && npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@mariozechner/pi-agent-core": "^0.11.2",
26
- "@mariozechner/pi-ai": "^0.11.2",
27
- "@mariozechner/pi-tui": "^0.11.2",
25
+ "@mariozechner/pi-agent-core": "^0.11.3",
26
+ "@mariozechner/pi-ai": "^0.11.3",
27
+ "@mariozechner/pi-tui": "^0.11.3",
28
28
  "chalk": "^5.5.0",
29
29
  "diff": "^8.0.2",
30
30
  "glob": "^11.0.3"