@mariozechner/pi-coding-agent 0.29.1 → 0.30.1

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 (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +3 -1
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +4 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +3 -2
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +14 -11
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/model-resolver.d.ts +1 -1
  12. package/dist/core/model-resolver.d.ts.map +1 -1
  13. package/dist/core/model-resolver.js +2 -1
  14. package/dist/core/model-resolver.js.map +1 -1
  15. package/dist/core/sdk.d.ts.map +1 -1
  16. package/dist/core/sdk.js +8 -2
  17. package/dist/core/sdk.js.map +1 -1
  18. package/dist/core/session-manager.d.ts +26 -9
  19. package/dist/core/session-manager.d.ts.map +1 -1
  20. package/dist/core/session-manager.js +75 -37
  21. package/dist/core/session-manager.js.map +1 -1
  22. package/dist/main.d.ts.map +1 -1
  23. package/dist/main.js +7 -3
  24. package/dist/main.js.map +1 -1
  25. package/dist/modes/interactive/components/custom-editor.d.ts +2 -0
  26. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  27. package/dist/modes/interactive/components/custom-editor.js +13 -1
  28. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  29. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  30. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  31. package/dist/modes/interactive/components/model-selector.js +4 -3
  32. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  33. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.js +10 -5
  35. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  36. package/docs/sdk.md +6 -3
  37. package/docs/session-tree.md +452 -0
  38. package/examples/sdk/11-sessions.ts +5 -3
  39. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- import { Editor, isCtrlC, isCtrlD, isCtrlG, isCtrlO, isCtrlP, isCtrlT, isCtrlZ, isEscape, isShiftTab, } from "@mariozechner/pi-tui";
1
+ import { Editor, isCtrlC, isCtrlD, isCtrlG, isCtrlL, isCtrlO, isCtrlP, isCtrlT, isCtrlZ, isEscape, isShiftCtrlP, isShiftTab, } from "@mariozechner/pi-tui";
2
2
  /**
3
3
  * Custom editor that handles Escape and Ctrl+C keys for coding-agent
4
4
  */
@@ -8,6 +8,8 @@ export class CustomEditor extends Editor {
8
8
  onCtrlD;
9
9
  onShiftTab;
10
10
  onCtrlP;
11
+ onShiftCtrlP;
12
+ onCtrlL;
11
13
  onCtrlO;
12
14
  onCtrlT;
13
15
  onCtrlG;
@@ -28,11 +30,21 @@ export class CustomEditor extends Editor {
28
30
  this.onCtrlT();
29
31
  return;
30
32
  }
33
+ // Intercept Ctrl+L for model selector
34
+ if (isCtrlL(data) && this.onCtrlL) {
35
+ this.onCtrlL();
36
+ return;
37
+ }
31
38
  // Intercept Ctrl+O for tool output expansion
32
39
  if (isCtrlO(data) && this.onCtrlO) {
33
40
  this.onCtrlO();
34
41
  return;
35
42
  }
43
+ // Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
44
+ if (isShiftCtrlP(data) && this.onShiftCtrlP) {
45
+ this.onShiftCtrlP();
46
+ return;
47
+ }
36
48
  // Intercept Ctrl+P for model cycling
37
49
  if (isCtrlP(data) && this.onCtrlP) {
38
50
  this.onCtrlP();
@@ -1 +1 @@
1
- {"version":3,"file":"custom-editor.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,QAAQ,EACR,UAAU,GACV,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAM;IAChC,QAAQ,CAAc;IACtB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,UAAU,CAAc;IACxB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,OAAO,CAAc;IAE5B,WAAW,CAAC,IAAY,EAAQ;QAC/B,uCAAuC;QACvC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,+BAA+B;QAC/B,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,wDAAwD;QACxD,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,6CAA6C;QAC7C,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,qCAAqC;QACrC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,iDAAiD;QACjD,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,OAAO;QACR,CAAC;QAED,gEAAgE;QAChE,2DAA2D;QAC3D,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;YACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO;QACR,CAAC;QAED,mBAAmB;QACnB,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,+CAA+C;QAC/C,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjD,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,CAAC;YACD,+CAA+C;YAC/C,OAAO;QACR,CAAC;QAED,qCAAqC;QACrC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CACxB;CACD","sourcesContent":["import {\n\tEditor,\n\tisCtrlC,\n\tisCtrlD,\n\tisCtrlG,\n\tisCtrlO,\n\tisCtrlP,\n\tisCtrlT,\n\tisCtrlZ,\n\tisEscape,\n\tisShiftTab,\n} from \"@mariozechner/pi-tui\";\n\n/**\n * Custom editor that handles Escape and Ctrl+C keys for coding-agent\n */\nexport class CustomEditor extends Editor {\n\tpublic onEscape?: () => void;\n\tpublic onCtrlC?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onShiftTab?: () => void;\n\tpublic onCtrlP?: () => void;\n\tpublic onCtrlO?: () => void;\n\tpublic onCtrlT?: () => void;\n\tpublic onCtrlG?: () => void;\n\tpublic onCtrlZ?: () => void;\n\n\thandleInput(data: string): void {\n\t\t// Intercept Ctrl+G for external editor\n\t\tif (isCtrlG(data) && this.onCtrlG) {\n\t\t\tthis.onCtrlG();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+Z for suspend\n\t\tif (isCtrlZ(data) && this.onCtrlZ) {\n\t\t\tthis.onCtrlZ();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+T for thinking block visibility toggle\n\t\tif (isCtrlT(data) && this.onCtrlT) {\n\t\t\tthis.onCtrlT();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+O for tool output expansion\n\t\tif (isCtrlO(data) && this.onCtrlO) {\n\t\t\tthis.onCtrlO();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+P for model cycling\n\t\tif (isCtrlP(data) && this.onCtrlP) {\n\t\t\tthis.onCtrlP();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Shift+Tab for thinking level cycling\n\t\tif (isShiftTab(data) && this.onShiftTab) {\n\t\t\tthis.onShiftTab();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Escape key - but only if autocomplete is NOT active\n\t\t// (let parent handle escape for autocomplete cancellation)\n\t\tif (isEscape(data) && this.onEscape && !this.isShowingAutocomplete()) {\n\t\t\tthis.onEscape();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+C\n\t\tif (isCtrlC(data) && this.onCtrlC) {\n\t\t\tthis.onCtrlC();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+D (only when editor is empty)\n\t\tif (isCtrlD(data)) {\n\t\t\tif (this.getText().length === 0 && this.onCtrlD) {\n\t\t\t\tthis.onCtrlD();\n\t\t\t}\n\t\t\t// Always consume Ctrl+D (don't pass to parent)\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to parent for normal handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-editor.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,UAAU,GACV,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAM;IAChC,QAAQ,CAAc;IACtB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,UAAU,CAAc;IACxB,OAAO,CAAc;IACrB,YAAY,CAAc;IAC1B,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,OAAO,CAAc;IACrB,OAAO,CAAc;IAE5B,WAAW,CAAC,IAAY,EAAQ;QAC/B,uCAAuC;QACvC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,+BAA+B;QAC/B,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,wDAAwD;QACxD,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,sCAAsC;QACtC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,6CAA6C;QAC7C,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,0EAA0E;QAC1E,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YAC7C,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,OAAO;QACR,CAAC;QAED,qCAAqC;QACrC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,iDAAiD;QACjD,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,OAAO;QACR,CAAC;QAED,gEAAgE;QAChE,2DAA2D;QAC3D,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;YACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO;QACR,CAAC;QAED,mBAAmB;QACnB,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,+CAA+C;QAC/C,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjD,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,CAAC;YACD,+CAA+C;YAC/C,OAAO;QACR,CAAC;QAED,qCAAqC;QACrC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CACxB;CACD","sourcesContent":["import {\n\tEditor,\n\tisCtrlC,\n\tisCtrlD,\n\tisCtrlG,\n\tisCtrlL,\n\tisCtrlO,\n\tisCtrlP,\n\tisCtrlT,\n\tisCtrlZ,\n\tisEscape,\n\tisShiftCtrlP,\n\tisShiftTab,\n} from \"@mariozechner/pi-tui\";\n\n/**\n * Custom editor that handles Escape and Ctrl+C keys for coding-agent\n */\nexport class CustomEditor extends Editor {\n\tpublic onEscape?: () => void;\n\tpublic onCtrlC?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onShiftTab?: () => void;\n\tpublic onCtrlP?: () => void;\n\tpublic onShiftCtrlP?: () => void;\n\tpublic onCtrlL?: () => void;\n\tpublic onCtrlO?: () => void;\n\tpublic onCtrlT?: () => void;\n\tpublic onCtrlG?: () => void;\n\tpublic onCtrlZ?: () => void;\n\n\thandleInput(data: string): void {\n\t\t// Intercept Ctrl+G for external editor\n\t\tif (isCtrlG(data) && this.onCtrlG) {\n\t\t\tthis.onCtrlG();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+Z for suspend\n\t\tif (isCtrlZ(data) && this.onCtrlZ) {\n\t\t\tthis.onCtrlZ();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+T for thinking block visibility toggle\n\t\tif (isCtrlT(data) && this.onCtrlT) {\n\t\t\tthis.onCtrlT();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+L for model selector\n\t\tif (isCtrlL(data) && this.onCtrlL) {\n\t\t\tthis.onCtrlL();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+O for tool output expansion\n\t\tif (isCtrlO(data) && this.onCtrlO) {\n\t\t\tthis.onCtrlO();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)\n\t\tif (isShiftCtrlP(data) && this.onShiftCtrlP) {\n\t\t\tthis.onShiftCtrlP();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+P for model cycling\n\t\tif (isCtrlP(data) && this.onCtrlP) {\n\t\t\tthis.onCtrlP();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Shift+Tab for thinking level cycling\n\t\tif (isShiftTab(data) && this.onShiftTab) {\n\t\t\tthis.onShiftTab();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Escape key - but only if autocomplete is NOT active\n\t\t// (let parent handle escape for autocomplete cancellation)\n\t\tif (isEscape(data) && this.onEscape && !this.isShowingAutocomplete()) {\n\t\t\tthis.onEscape();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+C\n\t\tif (isCtrlC(data) && this.onCtrlC) {\n\t\t\tthis.onCtrlC();\n\t\t\treturn;\n\t\t}\n\n\t\t// Intercept Ctrl+D (only when editor is empty)\n\t\tif (isCtrlD(data)) {\n\t\t\tif (this.getText().length === 0 && this.onCtrlD) {\n\t\t\t\tthis.onCtrlD();\n\t\t\t}\n\t\t\t// Always consume Ctrl+D (don't pass to parent)\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to parent for normal handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { Model } from "@mariozechner/pi-ai";
1
+ import { type Model } from "@mariozechner/pi-ai";
2
2
  import { Container, Input, type TUI } from "@mariozechner/pi-tui";
3
3
  import type { ModelRegistry } from "../../../core/model-registry.js";
4
4
  import type { SettingsManager } from "../../../core/settings-manager.js";
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EACN,SAAS,EACT,KAAK,EAOL,KAAK,GAAG,EACR,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;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,aAAa,CAAgB;IACrC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,YAAY,CAAiC;IAErD,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,aAAa,CAAC,eAAe,CAAC,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EAmDpB;YAEa,UAAU;IAiDxB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,UAAU;IAqDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA2BjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport {\n\tContainer,\n\tInput,\n\tisArrowDown,\n\tisArrowUp,\n\tisEnter,\n\tisEscape,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@mariozechner/pi-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel: string;\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 modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\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.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\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 model filtering\n\t\tconst hintText =\n\t\t\tscopedModels.length > 0\n\t\t\t\t? \"Showing models from --models scope\"\n\t\t\t\t: \"Only showing models with configured API keys (see README for details)\";\n\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\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\tlet models: ModelItem[];\n\n\t\t// Use scoped models if provided via --models flag\n\t\tif (this.scopedModels.length > 0) {\n\t\t\tmodels = this.scopedModels.map((scoped) => ({\n\t\t\t\tprovider: scoped.model.provider,\n\t\t\t\tid: scoped.model.id,\n\t\t\t\tmodel: scoped.model,\n\t\t\t}));\n\t\t} else {\n\t\t\t// Refresh to pick up any changes to models.json\n\t\t\tthis.modelRegistry.refresh();\n\n\t\t\t// Check for models.json errors\n\t\t\tconst loadError = this.modelRegistry.getError();\n\t\t\tif (loadError) {\n\t\t\t\tthis.errorMessage = loadError;\n\t\t\t}\n\n\t\t\t// Load available models (built-in models still work even if models.json failed)\n\t\t\ttry {\n\t\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\tid: model.id,\n\t\t\t\t\tmodel,\n\t\t\t\t}));\n\t\t\t} catch (error) {\n\t\t\t\tthis.allModels = [];\n\t\t\t\tthis.filteredModels = [];\n\t\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Sort: current model first, then by provider\n\t\tmodels.sort((a, b) => {\n\t\t\tconst aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;\n\t\t\tconst bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = this.currentModel?.id === item.model.id;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${prefix + theme.fg(\"accent\", modelText)} ${providerBadge}${checkmark}`;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${modelText} ${providerBadge}${checkmark}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (isArrowUp(keyData)) {\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 (isArrowDown(keyData)) {\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 (isEnter(keyData)) {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAkB,MAAM,qBAAqB,CAAC;AACjE,OAAO,EACN,SAAS,EACT,KAAK,EAOL,KAAK,GAAG,EACR,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;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,aAAa,CAAgB;IACrC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,YAAY,CAAiC;IAErD,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAC/B,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,aAAa,CAAC,eAAe,CAAC,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EAmDpB;YAEa,UAAU;IAiDxB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,UAAU;IAqDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA2BjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import { type Model, modelsAreEqual } from \"@mariozechner/pi-ai\";\nimport {\n\tContainer,\n\tInput,\n\tisArrowDown,\n\tisArrowUp,\n\tisEnter,\n\tisEscape,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@mariozechner/pi-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel: string;\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 modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\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.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\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 model filtering\n\t\tconst hintText =\n\t\t\tscopedModels.length > 0\n\t\t\t\t? \"Showing models from --models scope\"\n\t\t\t\t: \"Only showing models with configured API keys (see README for details)\";\n\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\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\tlet models: ModelItem[];\n\n\t\t// Use scoped models if provided via --models flag\n\t\tif (this.scopedModels.length > 0) {\n\t\t\tmodels = this.scopedModels.map((scoped) => ({\n\t\t\t\tprovider: scoped.model.provider,\n\t\t\t\tid: scoped.model.id,\n\t\t\t\tmodel: scoped.model,\n\t\t\t}));\n\t\t} else {\n\t\t\t// Refresh to pick up any changes to models.json\n\t\t\tthis.modelRegistry.refresh();\n\n\t\t\t// Check for models.json errors\n\t\t\tconst loadError = this.modelRegistry.getError();\n\t\t\tif (loadError) {\n\t\t\t\tthis.errorMessage = loadError;\n\t\t\t}\n\n\t\t\t// Load available models (built-in models still work even if models.json failed)\n\t\t\ttry {\n\t\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\tid: model.id,\n\t\t\t\t\tmodel,\n\t\t\t\t}));\n\t\t\t} catch (error) {\n\t\t\t\tthis.allModels = [];\n\t\t\t\tthis.filteredModels = [];\n\t\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn;\n\t\t\t}\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 = modelsAreEqual(this.currentModel, a.model);\n\t\t\tconst bIsCurrent = modelsAreEqual(this.currentModel, b.model);\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = modelsAreEqual(this.currentModel, item.model);\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 (isArrowUp(keyData)) {\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 (isArrowDown(keyData)) {\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 (isEnter(keyData)) {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { modelsAreEqual } from "@mariozechner/pi-ai";
1
2
  import { Container, Input, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text, } from "@mariozechner/pi-tui";
2
3
  import { fuzzyFilter } from "../../../utils/fuzzy.js";
3
4
  import { theme } from "../theme/theme.js";
@@ -96,8 +97,8 @@ export class ModelSelectorComponent extends Container {
96
97
  }
97
98
  // Sort: current model first, then by provider
98
99
  models.sort((a, b) => {
99
- const aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;
100
- const bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;
100
+ const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
101
+ const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
101
102
  if (aIsCurrent && !bIsCurrent)
102
103
  return -1;
103
104
  if (!aIsCurrent && bIsCurrent)
@@ -123,7 +124,7 @@ export class ModelSelectorComponent extends Container {
123
124
  if (!item)
124
125
  continue;
125
126
  const isSelected = i === this.selectedIndex;
126
- const isCurrent = this.currentModel?.id === item.model.id;
127
+ const isCurrent = modelsAreEqual(this.currentModel, item.model);
127
128
  let line = "";
128
129
  if (isSelected) {
129
130
  const prefix = theme.fg("accent", "→ ");
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AACA,OAAO,EACN,SAAS,EACT,KAAK,EACL,WAAW,EACX,SAAS,EACT,OAAO,EACP,QAAQ,EACR,MAAM,EACN,IAAI,GAEJ,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAapD;;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,aAAa,CAAgB;IAC7B,gBAAgB,CAA8B;IAC9C,gBAAgB,CAAa;IAC7B,YAAY,GAAkB,IAAI,CAAC;IACnC,GAAG,CAAM;IACT,YAAY,CAAiC;IAErD,YACC,GAAQ,EACR,YAA+B,EAC/B,eAAgC,EAChC,aAA4B,EAC5B,YAA4C,EAC5C,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,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,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,iCAAiC;QACjC,MAAM,QAAQ,GACb,YAAY,CAAC,MAAM,GAAG,CAAC;YACtB,CAAC,CAAC,oCAAoC;YACtC,CAAC,CAAC,uEAAuE,CAAC;QAC5E,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7D,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,IAAI,MAAmB,CAAC;QAExB,kDAAkD;QAClD,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAC3C,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ;gBAC/B,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE;gBACnB,KAAK,EAAE,MAAM,CAAC,KAAK;aACnB,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACP,gDAAgD;YAChD,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;YAE7B,+BAA+B;YAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC;YAChD,IAAI,SAAS,EAAE,CAAC;gBACf,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;YAC/B,CAAC;YAED,gFAAgF;YAChF,IAAI,CAAC;gBACJ,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;gBAChE,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,KAAiB,EAAE,EAAE,CAAC,CAAC;oBACpD,QAAQ,EAAE,KAAK,CAAC,QAAQ;oBACxB,EAAE,EAAE,KAAK,CAAC,EAAE;oBACZ,KAAK;iBACL,CAAC,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;gBACpB,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;gBACzB,IAAI,CAAC,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC3E,OAAO;YACR,CAAC;QACF,CAAC;QAED,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,cAAc,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC,CAAC;QACpG,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,GAAG,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,aAAa,GAAG,SAAS,EAAE,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,GAAG,SAAS,IAAI,aAAa,GAAG,SAAS,EAAE,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,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YACxB,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,WAAW,CAAC,OAAO,CAAC,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,CAAC,OAAO,CAAC,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,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,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 {\n\tContainer,\n\tInput,\n\tisArrowDown,\n\tisArrowUp,\n\tisEnter,\n\tisEscape,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@mariozechner/pi-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel: string;\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 modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\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.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\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 model filtering\n\t\tconst hintText =\n\t\t\tscopedModels.length > 0\n\t\t\t\t? \"Showing models from --models scope\"\n\t\t\t\t: \"Only showing models with configured API keys (see README for details)\";\n\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\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\tlet models: ModelItem[];\n\n\t\t// Use scoped models if provided via --models flag\n\t\tif (this.scopedModels.length > 0) {\n\t\t\tmodels = this.scopedModels.map((scoped) => ({\n\t\t\t\tprovider: scoped.model.provider,\n\t\t\t\tid: scoped.model.id,\n\t\t\t\tmodel: scoped.model,\n\t\t\t}));\n\t\t} else {\n\t\t\t// Refresh to pick up any changes to models.json\n\t\t\tthis.modelRegistry.refresh();\n\n\t\t\t// Check for models.json errors\n\t\t\tconst loadError = this.modelRegistry.getError();\n\t\t\tif (loadError) {\n\t\t\t\tthis.errorMessage = loadError;\n\t\t\t}\n\n\t\t\t// Load available models (built-in models still work even if models.json failed)\n\t\t\ttry {\n\t\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\tid: model.id,\n\t\t\t\t\tmodel,\n\t\t\t\t}));\n\t\t\t} catch (error) {\n\t\t\t\tthis.allModels = [];\n\t\t\t\tthis.filteredModels = [];\n\t\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Sort: current model first, then by provider\n\t\tmodels.sort((a, b) => {\n\t\t\tconst aIsCurrent = this.currentModel?.id === a.model.id && this.currentModel?.provider === a.provider;\n\t\t\tconst bIsCurrent = this.currentModel?.id === b.model.id && this.currentModel?.provider === b.provider;\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = this.currentModel?.id === item.model.id;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${prefix + theme.fg(\"accent\", modelText)} ${providerBadge}${checkmark}`;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${modelText} ${providerBadge}${checkmark}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (isArrowUp(keyData)) {\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 (isArrowDown(keyData)) {\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 (isEnter(keyData)) {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Model, modelsAreEqual } from \"@mariozechner/pi-ai\";\nimport {\n\tContainer,\n\tInput,\n\tisArrowDown,\n\tisArrowUp,\n\tisEnter,\n\tisEscape,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@mariozechner/pi-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.js\";\nimport type { SettingsManager } from \"../../../core/settings-manager.js\";\nimport { fuzzyFilter } from \"../../../utils/fuzzy.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel: string;\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 modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | null = null;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | null,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\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.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\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 model filtering\n\t\tconst hintText =\n\t\t\tscopedModels.length > 0\n\t\t\t\t? \"Showing models from --models scope\"\n\t\t\t\t: \"Only showing models with configured API keys (see README for details)\";\n\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\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\tlet models: ModelItem[];\n\n\t\t// Use scoped models if provided via --models flag\n\t\tif (this.scopedModels.length > 0) {\n\t\t\tmodels = this.scopedModels.map((scoped) => ({\n\t\t\t\tprovider: scoped.model.provider,\n\t\t\t\tid: scoped.model.id,\n\t\t\t\tmodel: scoped.model,\n\t\t\t}));\n\t\t} else {\n\t\t\t// Refresh to pick up any changes to models.json\n\t\t\tthis.modelRegistry.refresh();\n\n\t\t\t// Check for models.json errors\n\t\t\tconst loadError = this.modelRegistry.getError();\n\t\t\tif (loadError) {\n\t\t\t\tthis.errorMessage = loadError;\n\t\t\t}\n\n\t\t\t// Load available models (built-in models still work even if models.json failed)\n\t\t\ttry {\n\t\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\tid: model.id,\n\t\t\t\t\tmodel,\n\t\t\t\t}));\n\t\t\t} catch (error) {\n\t\t\t\tthis.allModels = [];\n\t\t\t\tthis.filteredModels = [];\n\t\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn;\n\t\t\t}\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 = modelsAreEqual(this.currentModel, a.model);\n\t\t\tconst bIsCurrent = modelsAreEqual(this.currentModel, b.model);\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\n\t\tthis.allModels = models;\n\t\tthis.filteredModels = models;\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = modelsAreEqual(this.currentModel, item.model);\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 (isArrowUp(keyData)) {\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 (isArrowDown(keyData)) {\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 (isEnter(keyData)) {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}