apex-code 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/core/settings-manager.d.ts.map +1 -1
  3. package/dist/core/settings-manager.js +1 -1
  4. package/dist/core/settings-manager.js.map +1 -1
  5. package/dist/core/tools/bash.d.ts +2 -7
  6. package/dist/core/tools/bash.d.ts.map +1 -1
  7. package/dist/core/tools/bash.js +8 -30
  8. package/dist/core/tools/bash.js.map +1 -1
  9. package/dist/core/tools/delegate.d.ts.map +1 -1
  10. package/dist/core/tools/delegate.js +2 -2
  11. package/dist/core/tools/delegate.js.map +1 -1
  12. package/dist/core/tools/find.d.ts.map +1 -1
  13. package/dist/core/tools/find.js +4 -4
  14. package/dist/core/tools/find.js.map +1 -1
  15. package/dist/core/tools/grep.d.ts.map +1 -1
  16. package/dist/core/tools/grep.js +4 -4
  17. package/dist/core/tools/grep.js.map +1 -1
  18. package/dist/core/tools/ls.d.ts.map +1 -1
  19. package/dist/core/tools/ls.js +4 -4
  20. package/dist/core/tools/ls.js.map +1 -1
  21. package/dist/core/tools/read.d.ts.map +1 -1
  22. package/dist/core/tools/read.js +11 -8
  23. package/dist/core/tools/read.js.map +1 -1
  24. package/dist/core/tools/write.d.ts.map +1 -1
  25. package/dist/core/tools/write.js +4 -5
  26. package/dist/core/tools/write.js.map +1 -1
  27. package/dist/extensions/llama/ui.d.ts.map +1 -1
  28. package/dist/extensions/llama/ui.js +3 -2
  29. package/dist/extensions/llama/ui.js.map +1 -1
  30. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  31. package/dist/modes/interactive/components/assistant-message.js +0 -10
  32. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  33. package/dist/modes/interactive/components/autocomplete-source.d.ts +8 -0
  34. package/dist/modes/interactive/components/autocomplete-source.d.ts.map +1 -0
  35. package/dist/modes/interactive/components/autocomplete-source.js +23 -0
  36. package/dist/modes/interactive/components/autocomplete-source.js.map +1 -0
  37. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  38. package/dist/modes/interactive/components/bash-execution.js +6 -4
  39. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  40. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  41. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  42. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  43. package/dist/modes/interactive/components/composer-dock.d.ts +12 -0
  44. package/dist/modes/interactive/components/composer-dock.d.ts.map +1 -0
  45. package/dist/modes/interactive/components/composer-dock.js +24 -0
  46. package/dist/modes/interactive/components/composer-dock.js.map +1 -0
  47. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/config-selector.js +11 -9
  49. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  50. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  51. package/dist/modes/interactive/components/extension-editor.js +9 -9
  52. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  53. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  54. package/dist/modes/interactive/components/extension-input.js +9 -5
  55. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  56. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/extension-selector.js +11 -10
  58. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  59. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/first-time-setup.js +6 -6
  61. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  62. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/footer.js +8 -3
  64. package/dist/modes/interactive/components/footer.js.map +1 -1
  65. package/dist/modes/interactive/components/keybinding-hints.d.ts +21 -0
  66. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/keybinding-hints.js +34 -0
  68. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  69. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  70. package/dist/modes/interactive/components/login-dialog.js +27 -23
  71. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  72. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  73. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/model-selector.js +14 -18
  75. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  76. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/oauth-selector.js +7 -6
  78. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  79. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -1
  80. package/dist/modes/interactive/components/permission-preview.js +3 -3
  81. package/dist/modes/interactive/components/permission-preview.js.map +1 -1
  82. package/dist/modes/interactive/components/prompt-input.d.ts +12 -0
  83. package/dist/modes/interactive/components/prompt-input.d.ts.map +1 -0
  84. package/dist/modes/interactive/components/prompt-input.js +18 -0
  85. package/dist/modes/interactive/components/prompt-input.js.map +1 -0
  86. package/dist/modes/interactive/components/scoped-models-selector.d.ts +1 -1
  87. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  88. package/dist/modes/interactive/components/scoped-models-selector.js +4 -3
  89. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  90. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  91. package/dist/modes/interactive/components/session-selector.js +26 -19
  92. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  93. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/settings-selector.js +39 -6
  95. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  96. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
  97. package/dist/modes/interactive/components/settings-submenu.js +8 -6
  98. package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
  99. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  100. package/dist/modes/interactive/components/thinking-selector.js +10 -5
  101. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  102. package/dist/modes/interactive/components/tool-execution.d.ts +1 -11
  103. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/tool-execution.js +9 -38
  105. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  106. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/tool-panel.js +7 -2
  108. package/dist/modes/interactive/components/tool-panel.js.map +1 -1
  109. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/tree-selector.js +9 -5
  111. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  112. package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/trust-selector.js +11 -11
  114. package/dist/modes/interactive/components/trust-selector.js.map +1 -1
  115. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  116. package/dist/modes/interactive/components/user-message-selector.js +2 -2
  117. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  118. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/user-message.js +6 -3
  120. package/dist/modes/interactive/components/user-message.js.map +1 -1
  121. package/dist/modes/interactive/interactive-mode.d.ts +7 -2
  122. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  123. package/dist/modes/interactive/interactive-mode.js +64 -64
  124. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  125. package/dist/modes/print-mode.d.ts.map +1 -1
  126. package/dist/modes/print-mode.js +49 -23
  127. package/dist/modes/print-mode.js.map +1 -1
  128. package/docs/json.md +36 -0
  129. package/docs/settings.md +1 -1
  130. package/npm-shrinkwrap.json +5 -5
  131. package/package.json +2 -2
  132. package/dist/modes/interactive/components/message-spine.d.ts +0 -14
  133. package/dist/modes/interactive/components/message-spine.d.ts.map +0 -1
  134. package/dist/modes/interactive/components/message-spine.js +0 -19
  135. package/dist/modes/interactive/components/message-spine.js.map +0 -1
@@ -16,4 +16,25 @@ export declare function keyText(keybinding: Keybinding, options?: KeyTextFormatO
16
16
  export declare function keyDisplayText(keybinding: Keybinding): string;
17
17
  export declare function keyHint(keybinding: Keybinding, description: string): string;
18
18
  export declare function rawKeyHint(key: string, description: string): string;
19
+ /**
20
+ * The one shape for "output is hidden here": `... (12 more lines, ctrl+o to expand)`.
21
+ * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can
22
+ * append it unconditionally.
23
+ */
24
+ export declare function formatHiddenLines(count: number, position: "more" | "earlier" | "all"): string;
25
+ /** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */
26
+ export declare function previewLineCount(total: number, limit: number): number;
27
+ /** A binding, or a key that is not one. */
28
+ export type HintKeyPart = Keybinding | {
29
+ literal: string;
30
+ };
31
+ /** One key, or several shown as `a/b` by their primary binding. */
32
+ export type HintKey = HintKeyPart | readonly HintKeyPart[];
33
+ /**
34
+ * The one grammar for a row of key hints: `up/down move · enter select · escape close`.
35
+ * Keys come from the live bindings, so a rebound key is reported as the user bound it.
36
+ */
37
+ export declare function hintRow(hints: ReadonlyArray<readonly [key: HintKey, action: string]>, options?: {
38
+ ascii?: boolean;
39
+ }): string;
19
40
  //# sourceMappingURL=keybinding-hints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"keybinding-hints.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,UAAU,EAAc,MAAM,wBAAwB,CAAC;AAGrF,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAOD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAUrF;AAOD,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAG1F;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAE7D;AAED,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEnE","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n"]}
1
+ {"version":3,"file":"keybinding-hints.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,UAAU,EAAc,MAAM,wBAAwB,CAAC;AAGrF,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAOD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAUrF;AAOD,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAG1F;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAE7D;AAED,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAK7F;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,mEAAmE;AACnE,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,SAAS,WAAW,EAAE,CAAC;AAM3D;;;GAGG;AACH,wBAAgB,OAAO,CACtB,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAC7D,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,MAAM,CAUR","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n\n/**\n * The one shape for \"output is hidden here\": `... (12 more lines, ctrl+o to expand)`.\n * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can\n * append it unconditionally.\n */\nexport function formatHiddenLines(count: number, position: \"more\" | \"earlier\" | \"all\"): string {\n\tif (count <= 0) return \"\";\n\tconst noun = count === 1 ? \"line\" : \"lines\";\n\tconst counted = position === \"all\" ? `${count} ${noun}` : `${count} ${position} ${noun}`;\n\treturn `${theme.fg(\"muted\", `... (${counted},`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n}\n\n/** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */\nexport function previewLineCount(total: number, limit: number): number {\n\treturn total <= limit + 1 ? total : limit;\n}\n\n/** A binding, or a key that is not one. */\nexport type HintKeyPart = Keybinding | { literal: string };\n\n/** One key, or several shown as `a/b` by their primary binding. */\nexport type HintKey = HintKeyPart | readonly HintKeyPart[];\n\nfunction hintKeyText(part: HintKeyPart, options: KeyTextFormatOptions): string {\n\treturn typeof part === \"string\" ? keyText(part, options) : formatKeyText(part.literal);\n}\n\n/**\n * The one grammar for a row of key hints: `up/down move · enter select · escape close`.\n * Keys come from the live bindings, so a rebound key is reported as the user bound it.\n */\nexport function hintRow(\n\thints: ReadonlyArray<readonly [key: HintKey, action: string]>,\n\toptions: { ascii?: boolean } = {},\n): string {\n\tconst separator = theme.fg(\"borderMuted\", options.ascii ? \" - \" : \" · \");\n\treturn hints\n\t\t.map(([key, action]) => {\n\t\t\tconst text = Array.isArray(key)\n\t\t\t\t? key.map((part: HintKeyPart) => hintKeyText(part, { primaryOnly: true })).join(\"/\")\n\t\t\t\t: hintKeyText(key as HintKeyPart, {});\n\t\t\treturn `${theme.fg(\"dim\", text)}${theme.fg(\"muted\", ` ${action}`)}`;\n\t\t})\n\t\t.join(separator);\n}\n"]}
@@ -34,4 +34,38 @@ export function keyHint(keybinding, description) {
34
34
  export function rawKeyHint(key, description) {
35
35
  return theme.fg("dim", formatKeyText(key)) + theme.fg("muted", ` ${description}`);
36
36
  }
37
+ /**
38
+ * The one shape for "output is hidden here": `... (12 more lines, ctrl+o to expand)`.
39
+ * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can
40
+ * append it unconditionally.
41
+ */
42
+ export function formatHiddenLines(count, position) {
43
+ if (count <= 0)
44
+ return "";
45
+ const noun = count === 1 ? "line" : "lines";
46
+ const counted = position === "all" ? `${count} ${noun}` : `${count} ${position} ${noun}`;
47
+ return `${theme.fg("muted", `... (${counted},`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
48
+ }
49
+ /** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */
50
+ export function previewLineCount(total, limit) {
51
+ return total <= limit + 1 ? total : limit;
52
+ }
53
+ function hintKeyText(part, options) {
54
+ return typeof part === "string" ? keyText(part, options) : formatKeyText(part.literal);
55
+ }
56
+ /**
57
+ * The one grammar for a row of key hints: `up/down move · enter select · escape close`.
58
+ * Keys come from the live bindings, so a rebound key is reported as the user bound it.
59
+ */
60
+ export function hintRow(hints, options = {}) {
61
+ const separator = theme.fg("borderMuted", options.ascii ? " - " : " · ");
62
+ return hints
63
+ .map(([key, action]) => {
64
+ const text = Array.isArray(key)
65
+ ? key.map((part) => hintKeyText(part, { primaryOnly: true })).join("/")
66
+ : hintKeyText(key, {});
67
+ return `${theme.fg("dim", text)}${theme.fg("muted", ` ${action}`)}`;
68
+ })
69
+ .join(separator);
70
+ }
37
71
  //# sourceMappingURL=keybinding-hints.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"keybinding-hints.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAA+B,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAY1C,SAAS,aAAa,CAAC,IAAY,EAAE,OAA6B,EAAU;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACpG,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;AAAA,CACrG;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,OAAO,GAAyB,EAAE,EAAU;IACtF,OAAO,GAAG;SACR,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACV,CAAC;SACC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;SAC3C,IAAI,CAAC,GAAG,CAAC,CACX;SACA,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CACZ;AAED,SAAS,UAAU,CAAC,IAAa,EAAE,OAAO,GAAyB,EAAE,EAAU;IAC9E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;AAAA,CAC9C;AAED,MAAM,UAAU,OAAO,CAAC,UAAsB,EAAE,OAAO,GAAyB,EAAE,EAAU;IAC3F,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClD,OAAO,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAAA,CAC1E;AAED,MAAM,UAAU,cAAc,CAAC,UAAsB,EAAU;IAC9D,OAAO,UAAU,CAAC,cAAc,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAAA,CAC9E;AAED,MAAM,UAAU,OAAO,CAAC,UAAsB,EAAE,WAAmB,EAAU;IAC5E,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC,CAAC;AAAA,CACnF;AAED,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,WAAmB,EAAU;IACpE,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC,CAAC;AAAA,CAClF","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n"]}
1
+ {"version":3,"file":"keybinding-hints.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n\n/**\n * The one shape for \"output is hidden here\": `... (12 more lines, ctrl+o to expand)`.\n * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can\n * append it unconditionally.\n */\nexport function formatHiddenLines(count: number, position: \"more\" | \"earlier\" | \"all\"): string {\n\tif (count <= 0) return \"\";\n\tconst noun = count === 1 ? \"line\" : \"lines\";\n\tconst counted = position === \"all\" ? `${count} ${noun}` : `${count} ${position} ${noun}`;\n\treturn `${theme.fg(\"muted\", `... (${counted},`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n}\n\n/** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */\nexport function previewLineCount(total: number, limit: number): number {\n\treturn total <= limit + 1 ? total : limit;\n}\n\n/** A binding, or a key that is not one. */\nexport type HintKeyPart = Keybinding | { literal: string };\n\n/** One key, or several shown as `a/b` by their primary binding. */\nexport type HintKey = HintKeyPart | readonly HintKeyPart[];\n\nfunction hintKeyText(part: HintKeyPart, options: KeyTextFormatOptions): string {\n\treturn typeof part === \"string\" ? keyText(part, options) : formatKeyText(part.literal);\n}\n\n/**\n * The one grammar for a row of key hints: `up/down move · enter select · escape close`.\n * Keys come from the live bindings, so a rebound key is reported as the user bound it.\n */\nexport function hintRow(\n\thints: ReadonlyArray<readonly [key: HintKey, action: string]>,\n\toptions: { ascii?: boolean } = {},\n): string {\n\tconst separator = theme.fg(\"borderMuted\", options.ascii ? \" - \" : \" · \");\n\treturn hints\n\t\t.map(([key, action]) => {\n\t\t\tconst text = Array.isArray(key)\n\t\t\t\t? key.map((part: HintKeyPart) => hintKeyText(part, { primaryOnly: true })).join(\"/\")\n\t\t\t\t: hintKeyText(key as HintKeyPart, {});\n\t\t\treturn `${theme.fg(\"dim\", text)}${theme.fg(\"muted\", ` ${action}`)}`;\n\t\t})\n\t\t.join(separator);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"login-dialog.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/login-dialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAuC,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAMlH;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAU,YAAW,SAAS;IACvE,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,aAAa,CAAC,CAA0B;IAChD,OAAO,CAAC,aAAa,CAAC,CAAyB;IAC/C,OAAO,CAAC,UAAU,CAA+C;IAGjE,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,YACC,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,EACxD,oBAAoB,CAAC,EAAE,MAAM,EAC7B,aAAa,CAAC,EAAE,MAAM,EAoCtB;IAED,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,MAAM;IAUd;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBjD;IAED;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAa9C;IAED;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/C;IAED;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBjE;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAOjC;IAED,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,YAAY,EAAO,EAAE,aAAa,UAAQ,GAAG,IAAI,CAa1F;IAED;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKjC;IAED;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGlC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU9B;CACD","sourcesContent":["import type { AuthInfoLink, OAuthDeviceCodeInfo } from \"@earendil-works/pi-ai\";\nimport { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport { openBrowser } from \"../../../utils/open-browser.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\n/**\n * Login dialog component - replaces editor during OAuth login flow\n */\nexport class LoginDialogComponent extends Container implements Focusable {\n\tprivate contentContainer: Container;\n\tprivate input: Input;\n\tprivate tui: TUI;\n\tprivate abortController = new AbortController();\n\tprivate inputResolver?: (value: string) => void;\n\tprivate inputRejecter?: (error: Error) => void;\n\tprivate onComplete: (success: boolean, message?: string) => void;\n\n\t// Focusable implementation - propagate to input for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tproviderId: string,\n\t\tonComplete: (success: boolean, message?: string) => void,\n\t\tproviderNameOverride?: string,\n\t\ttitleOverride?: string,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.onComplete = onComplete;\n\n\t\tconst providerName = providerNameOverride || providerId;\n\t\tconst title = titleOverride ?? `Login to ${providerName}`;\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(title)), 1, 0));\n\n\t\t// Dynamic content area\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Input (always present, used when needed)\n\t\tthis.input = new Input();\n\t\tthis.input.onSubmit = () => {\n\t\t\tif (this.inputResolver) {\n\t\t\t\tconst value = this.input.getValue();\n\t\t\t\tthis.replaceInputWithSubmittedText(value);\n\t\t\t\tthis.inputResolver(value);\n\t\t\t\tthis.inputResolver = undefined;\n\t\t\t\tthis.inputRejecter = undefined;\n\t\t\t}\n\t\t};\n\t\tthis.input.onEscape = () => {\n\t\t\tthis.cancel();\n\t\t};\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\tprivate replaceInputWithSubmittedText(value: string): void {\n\t\tthis.contentContainer.children = this.contentContainer.children.map((child) =>\n\t\t\tchild === this.input ? new Text(`> ${value}`, 0, 0) : child,\n\t\t);\n\t}\n\n\tprivate cancel(): void {\n\t\tthis.abortController.abort();\n\t\tif (this.inputRejecter) {\n\t\t\tthis.inputRejecter(new Error(\"Login cancelled\"));\n\t\t\tthis.inputResolver = undefined;\n\t\t\tthis.inputRejecter = undefined;\n\t\t}\n\t\tthis.onComplete(false, \"Login cancelled\");\n\t}\n\n\t/**\n\t * Called by onAuth callback - show URL and optional instructions\n\t */\n\tshowAuth(url: string, instructions?: string): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${url}\\x07${url}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 1, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${url}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 1, 0));\n\n\t\tif (instructions) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", instructions), 1, 0));\n\t\t}\n\n\t\topenBrowser(url);\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onDeviceCode callback - show URL and user code.\n\t */\n\tshowDeviceCode(info: OAuthDeviceCodeInfo): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${info.verificationUri}\\x07${info.verificationUri}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 1, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${info.verificationUri}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 1, 0));\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", `Enter code: ${info.userCode}`), 1, 0));\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show input for manual code/URL entry (for callback server providers)\n\t */\n\tshowManualInput(prompt: string): Promise<string> {\n\t\tthis.input.setValue(\"\");\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", prompt), 1, 0));\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to cancel\")})`, 1, 0));\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/**\n\t * Called by onPrompt callback - show prompt and wait for input\n\t * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)\n\t */\n\tshowPrompt(message: string, placeholder?: string): Promise<string> {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 1, 0));\n\t\tif (placeholder) {\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", `e.g., ${placeholder}`), 1, 0));\n\t\t}\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\t`(${keyHint(\"tui.select.cancel\", \"to cancel,\")} ${keyHint(\"tui.select.confirm\", \"to submit\")})`,\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.input.setValue(\"\");\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/** Show informational text before another login step. */\n\tshowDetails(lines: string[]): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tfor (const line of lines) {\n\t\t\tthis.contentContainer.addChild(new Text(line, 1, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Show provider-owned information and links without starting an auth callback flow. */\n\tshowInfo(message: string, links: readonly AuthInfoLink[] = [], showCloseHint = false): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 1, 0));\n\t\tfor (const link of links) {\n\t\t\tconst text = link.label ? `${link.label}: ${link.url}` : link.url;\n\t\t\tconst hyperlink = `\\x1b]8;;${link.url}\\x07${text}\\x1b]8;;\\x07`;\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", hyperlink), 1, 0));\n\t\t}\n\t\tif (showCloseHint) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to close\")})`, 1, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show waiting message (for polling flows like GitHub Copilot)\n\t */\n\tshowWaiting(message: string): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 1, 0));\n\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to cancel\")})`, 1, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onProgress callback\n\t */\n\tshowProgress(message: string): void {\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 1, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to input\n\t\tthis.input.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"login-dialog.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/login-dialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAA4C,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAOvH;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAU,YAAW,SAAS;IACvE,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,aAAa,CAAC,CAA0B;IAChD,OAAO,CAAC,aAAa,CAAC,CAAyB;IAC/C,OAAO,CAAC,UAAU,CAA+C;IAGjE,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,YACC,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,EACxD,oBAAoB,CAAC,EAAE,MAAM,EAC7B,aAAa,CAAC,EAAE,MAAM,EAoCtB;IAED,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,MAAM;IAUd;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBjD;IAED;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAa9C;IAED;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/C;IAED;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBjE;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAOjC;IAED,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,YAAY,EAAO,EAAE,aAAa,UAAQ,GAAG,IAAI,CAa1F;IAED;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKjC;IAED;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGlC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU9B;CACD","sourcesContent":["import type { AuthInfoLink, OAuthDeviceCodeInfo } from \"@earendil-works/pi-ai\";\nimport { Container, type Focusable, getKeybindings, type Input, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport { openBrowser } from \"../../../utils/open-browser.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { hintRow } from \"./keybinding-hints.ts\";\nimport { PromptInput, withComposerPrompt } from \"./prompt-input.ts\";\n\n/**\n * Login dialog component - replaces editor during OAuth login flow\n */\nexport class LoginDialogComponent extends Container implements Focusable {\n\tprivate contentContainer: Container;\n\tprivate input: Input;\n\tprivate tui: TUI;\n\tprivate abortController = new AbortController();\n\tprivate inputResolver?: (value: string) => void;\n\tprivate inputRejecter?: (error: Error) => void;\n\tprivate onComplete: (success: boolean, message?: string) => void;\n\n\t// Focusable implementation - propagate to input for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tproviderId: string,\n\t\tonComplete: (success: boolean, message?: string) => void,\n\t\tproviderNameOverride?: string,\n\t\ttitleOverride?: string,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.onComplete = onComplete;\n\n\t\tconst providerName = providerNameOverride || providerId;\n\t\tconst title = titleOverride ?? `Login to ${providerName}`;\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(title)), 0, 0));\n\n\t\t// Dynamic content area\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Input (always present, used when needed)\n\t\tthis.input = new PromptInput();\n\t\tthis.input.onSubmit = () => {\n\t\t\tif (this.inputResolver) {\n\t\t\t\tconst value = this.input.getValue();\n\t\t\t\tthis.replaceInputWithSubmittedText(value);\n\t\t\t\tthis.inputResolver(value);\n\t\t\t\tthis.inputResolver = undefined;\n\t\t\t\tthis.inputRejecter = undefined;\n\t\t\t}\n\t\t};\n\t\tthis.input.onEscape = () => {\n\t\t\tthis.cancel();\n\t\t};\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\tprivate replaceInputWithSubmittedText(value: string): void {\n\t\tthis.contentContainer.children = this.contentContainer.children.map((child) =>\n\t\t\tchild === this.input ? new Text(withComposerPrompt(`> ${value}`), 0, 0) : child,\n\t\t);\n\t}\n\n\tprivate cancel(): void {\n\t\tthis.abortController.abort();\n\t\tif (this.inputRejecter) {\n\t\t\tthis.inputRejecter(new Error(\"Login cancelled\"));\n\t\t\tthis.inputResolver = undefined;\n\t\t\tthis.inputRejecter = undefined;\n\t\t}\n\t\tthis.onComplete(false, \"Login cancelled\");\n\t}\n\n\t/**\n\t * Called by onAuth callback - show URL and optional instructions\n\t */\n\tshowAuth(url: string, instructions?: string): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${url}\\x07${url}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 0, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${url}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 0, 0));\n\n\t\tif (instructions) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", instructions), 0, 0));\n\t\t}\n\n\t\topenBrowser(url);\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onDeviceCode callback - show URL and user code.\n\t */\n\tshowDeviceCode(info: OAuthDeviceCodeInfo): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${info.verificationUri}\\x07${info.verificationUri}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 0, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${info.verificationUri}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 0, 0));\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", `Enter code: ${info.userCode}`), 0, 0));\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show input for manual code/URL entry (for callback server providers)\n\t */\n\tshowManualInput(prompt: string): Promise<string> {\n\t\tthis.input.setValue(\"\");\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", prompt), 0, 0));\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"cancel\"]]), 0, 0));\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/**\n\t * Called by onPrompt callback - show prompt and wait for input\n\t * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)\n\t */\n\tshowPrompt(message: string, placeholder?: string): Promise<string> {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 0, 0));\n\t\tif (placeholder) {\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", `e.g., ${placeholder}`), 0, 0));\n\t\t}\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\thintRow([\n\t\t\t\t\t[\"tui.select.confirm\", \"submit\"],\n\t\t\t\t\t[\"tui.select.cancel\", \"cancel\"],\n\t\t\t\t]),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.input.setValue(\"\");\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/** Show informational text before another login step. */\n\tshowDetails(lines: string[]): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tfor (const line of lines) {\n\t\t\tthis.contentContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Show provider-owned information and links without starting an auth callback flow. */\n\tshowInfo(message: string, links: readonly AuthInfoLink[] = [], showCloseHint = false): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 0, 0));\n\t\tfor (const link of links) {\n\t\t\tconst text = link.label ? `${link.label}: ${link.url}` : link.url;\n\t\t\tconst hyperlink = `\\x1b]8;;${link.url}\\x07${text}\\x1b]8;;\\x07`;\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", hyperlink), 0, 0));\n\t\t}\n\t\tif (showCloseHint) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"close\"]]), 0, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show waiting message (for polling flows like GitHub Copilot)\n\t */\n\tshowWaiting(message: string): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 0, 0));\n\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"cancel\"]]), 0, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onProgress callback\n\t */\n\tshowProgress(message: string): void {\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 0, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to input\n\t\tthis.input.handleInput(data);\n\t}\n}\n"]}
@@ -1,8 +1,9 @@
1
- import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
1
+ import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
2
2
  import { openBrowser } from "../../../utils/open-browser.js";
3
3
  import { theme } from "../theme/theme.js";
4
4
  import { DynamicBorder } from "./dynamic-border.js";
5
- import { keyHint } from "./keybinding-hints.js";
5
+ import { hintRow } from "./keybinding-hints.js";
6
+ import { PromptInput, withComposerPrompt } from "./prompt-input.js";
6
7
  /**
7
8
  * Login dialog component - replaces editor during OAuth login flow
8
9
  */
@@ -32,12 +33,12 @@ export class LoginDialogComponent extends Container {
32
33
  // Top border
33
34
  this.addChild(new DynamicBorder());
34
35
  // Title
35
- this.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
36
+ this.addChild(new Text(theme.fg("accent", theme.bold(title)), 0, 0));
36
37
  // Dynamic content area
37
38
  this.contentContainer = new Container();
38
39
  this.addChild(this.contentContainer);
39
40
  // Input (always present, used when needed)
40
- this.input = new Input();
41
+ this.input = new PromptInput();
41
42
  this.input.onSubmit = () => {
42
43
  if (this.inputResolver) {
43
44
  const value = this.input.getValue();
@@ -57,7 +58,7 @@ export class LoginDialogComponent extends Container {
57
58
  return this.abortController.signal;
58
59
  }
59
60
  replaceInputWithSubmittedText(value) {
60
- this.contentContainer.children = this.contentContainer.children.map((child) => child === this.input ? new Text(`> ${value}`, 0, 0) : child);
61
+ this.contentContainer.children = this.contentContainer.children.map((child) => child === this.input ? new Text(withComposerPrompt(`> ${value}`), 0, 0) : child);
61
62
  }
62
63
  cancel() {
63
64
  this.abortController.abort();
@@ -75,13 +76,13 @@ export class LoginDialogComponent extends Container {
75
76
  this.contentContainer.clear();
76
77
  this.contentContainer.addChild(new Spacer(1));
77
78
  const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
78
- this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 1, 0));
79
+ this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 0, 0));
79
80
  const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
80
81
  const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
81
- this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
82
+ this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 0, 0));
82
83
  if (instructions) {
83
84
  this.contentContainer.addChild(new Spacer(1));
84
- this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
85
+ this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 0, 0));
85
86
  }
86
87
  openBrowser(url);
87
88
  this.tui.requestRender();
@@ -93,12 +94,12 @@ export class LoginDialogComponent extends Container {
93
94
  this.contentContainer.clear();
94
95
  this.contentContainer.addChild(new Spacer(1));
95
96
  const linkedUrl = `\x1b]8;;${info.verificationUri}\x07${info.verificationUri}\x1b]8;;\x07`;
96
- this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 1, 0));
97
+ this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 0, 0));
97
98
  const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
98
99
  const hyperlink = `\x1b]8;;${info.verificationUri}\x07${clickHint}\x1b]8;;\x07`;
99
- this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
100
+ this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 0, 0));
100
101
  this.contentContainer.addChild(new Spacer(1));
101
- this.contentContainer.addChild(new Text(theme.fg("warning", `Enter code: ${info.userCode}`), 1, 0));
102
+ this.contentContainer.addChild(new Text(theme.fg("warning", `Enter code: ${info.userCode}`), 0, 0));
102
103
  this.tui.requestRender();
103
104
  }
104
105
  /**
@@ -107,9 +108,9 @@ export class LoginDialogComponent extends Container {
107
108
  showManualInput(prompt) {
108
109
  this.input.setValue("");
109
110
  this.contentContainer.addChild(new Spacer(1));
110
- this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
111
+ this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 0, 0));
111
112
  this.contentContainer.addChild(this.input);
112
- this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to cancel")})`, 1, 0));
113
+ this.contentContainer.addChild(new Text(hintRow([["tui.select.cancel", "cancel"]]), 0, 0));
113
114
  this.tui.requestRender();
114
115
  return new Promise((resolve, reject) => {
115
116
  this.inputResolver = resolve;
@@ -122,12 +123,15 @@ export class LoginDialogComponent extends Container {
122
123
  */
123
124
  showPrompt(message, placeholder) {
124
125
  this.contentContainer.addChild(new Spacer(1));
125
- this.contentContainer.addChild(new Text(theme.fg("text", message), 1, 0));
126
+ this.contentContainer.addChild(new Text(theme.fg("text", message), 0, 0));
126
127
  if (placeholder) {
127
- this.contentContainer.addChild(new Text(theme.fg("dim", `e.g., ${placeholder}`), 1, 0));
128
+ this.contentContainer.addChild(new Text(theme.fg("dim", `e.g., ${placeholder}`), 0, 0));
128
129
  }
129
130
  this.contentContainer.addChild(this.input);
130
- this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to cancel,")} ${keyHint("tui.select.confirm", "to submit")})`, 1, 0));
131
+ this.contentContainer.addChild(new Text(hintRow([
132
+ ["tui.select.confirm", "submit"],
133
+ ["tui.select.cancel", "cancel"],
134
+ ]), 0, 0));
131
135
  this.input.setValue("");
132
136
  this.tui.requestRender();
133
137
  return new Promise((resolve, reject) => {
@@ -140,22 +144,22 @@ export class LoginDialogComponent extends Container {
140
144
  this.contentContainer.clear();
141
145
  this.contentContainer.addChild(new Spacer(1));
142
146
  for (const line of lines) {
143
- this.contentContainer.addChild(new Text(line, 1, 0));
147
+ this.contentContainer.addChild(new Text(line, 0, 0));
144
148
  }
145
149
  this.tui.requestRender();
146
150
  }
147
151
  /** Show provider-owned information and links without starting an auth callback flow. */
148
152
  showInfo(message, links = [], showCloseHint = false) {
149
153
  this.contentContainer.addChild(new Spacer(1));
150
- this.contentContainer.addChild(new Text(theme.fg("text", message), 1, 0));
154
+ this.contentContainer.addChild(new Text(theme.fg("text", message), 0, 0));
151
155
  for (const link of links) {
152
156
  const text = link.label ? `${link.label}: ${link.url}` : link.url;
153
157
  const hyperlink = `\x1b]8;;${link.url}\x07${text}\x1b]8;;\x07`;
154
- this.contentContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
158
+ this.contentContainer.addChild(new Text(theme.fg("accent", hyperlink), 0, 0));
155
159
  }
156
160
  if (showCloseHint) {
157
161
  this.contentContainer.addChild(new Spacer(1));
158
- this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to close")})`, 1, 0));
162
+ this.contentContainer.addChild(new Text(hintRow([["tui.select.cancel", "close"]]), 0, 0));
159
163
  }
160
164
  this.tui.requestRender();
161
165
  }
@@ -164,15 +168,15 @@ export class LoginDialogComponent extends Container {
164
168
  */
165
169
  showWaiting(message) {
166
170
  this.contentContainer.addChild(new Spacer(1));
167
- this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
168
- this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to cancel")})`, 1, 0));
171
+ this.contentContainer.addChild(new Text(theme.fg("dim", message), 0, 0));
172
+ this.contentContainer.addChild(new Text(hintRow([["tui.select.cancel", "cancel"]]), 0, 0));
169
173
  this.tui.requestRender();
170
174
  }
171
175
  /**
172
176
  * Called by onProgress callback
173
177
  */
174
178
  showProgress(message) {
175
- this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
179
+ this.contentContainer.addChild(new Text(theme.fg("dim", message), 0, 0));
176
180
  this.tui.requestRender();
177
181
  }
178
182
  handleInput(data) {
@@ -1 +1 @@
1
- {"version":3,"file":"login-dialog.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/login-dialog.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AuthInfoLink, OAuthDeviceCodeInfo } from \"@earendil-works/pi-ai\";\nimport { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport { openBrowser } from \"../../../utils/open-browser.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\n/**\n * Login dialog component - replaces editor during OAuth login flow\n */\nexport class LoginDialogComponent extends Container implements Focusable {\n\tprivate contentContainer: Container;\n\tprivate input: Input;\n\tprivate tui: TUI;\n\tprivate abortController = new AbortController();\n\tprivate inputResolver?: (value: string) => void;\n\tprivate inputRejecter?: (error: Error) => void;\n\tprivate onComplete: (success: boolean, message?: string) => void;\n\n\t// Focusable implementation - propagate to input for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tproviderId: string,\n\t\tonComplete: (success: boolean, message?: string) => void,\n\t\tproviderNameOverride?: string,\n\t\ttitleOverride?: string,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.onComplete = onComplete;\n\n\t\tconst providerName = providerNameOverride || providerId;\n\t\tconst title = titleOverride ?? `Login to ${providerName}`;\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(title)), 1, 0));\n\n\t\t// Dynamic content area\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Input (always present, used when needed)\n\t\tthis.input = new Input();\n\t\tthis.input.onSubmit = () => {\n\t\t\tif (this.inputResolver) {\n\t\t\t\tconst value = this.input.getValue();\n\t\t\t\tthis.replaceInputWithSubmittedText(value);\n\t\t\t\tthis.inputResolver(value);\n\t\t\t\tthis.inputResolver = undefined;\n\t\t\t\tthis.inputRejecter = undefined;\n\t\t\t}\n\t\t};\n\t\tthis.input.onEscape = () => {\n\t\t\tthis.cancel();\n\t\t};\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\tprivate replaceInputWithSubmittedText(value: string): void {\n\t\tthis.contentContainer.children = this.contentContainer.children.map((child) =>\n\t\t\tchild === this.input ? new Text(`> ${value}`, 0, 0) : child,\n\t\t);\n\t}\n\n\tprivate cancel(): void {\n\t\tthis.abortController.abort();\n\t\tif (this.inputRejecter) {\n\t\t\tthis.inputRejecter(new Error(\"Login cancelled\"));\n\t\t\tthis.inputResolver = undefined;\n\t\t\tthis.inputRejecter = undefined;\n\t\t}\n\t\tthis.onComplete(false, \"Login cancelled\");\n\t}\n\n\t/**\n\t * Called by onAuth callback - show URL and optional instructions\n\t */\n\tshowAuth(url: string, instructions?: string): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${url}\\x07${url}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 1, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${url}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 1, 0));\n\n\t\tif (instructions) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", instructions), 1, 0));\n\t\t}\n\n\t\topenBrowser(url);\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onDeviceCode callback - show URL and user code.\n\t */\n\tshowDeviceCode(info: OAuthDeviceCodeInfo): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${info.verificationUri}\\x07${info.verificationUri}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 1, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${info.verificationUri}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 1, 0));\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", `Enter code: ${info.userCode}`), 1, 0));\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show input for manual code/URL entry (for callback server providers)\n\t */\n\tshowManualInput(prompt: string): Promise<string> {\n\t\tthis.input.setValue(\"\");\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", prompt), 1, 0));\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to cancel\")})`, 1, 0));\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/**\n\t * Called by onPrompt callback - show prompt and wait for input\n\t * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)\n\t */\n\tshowPrompt(message: string, placeholder?: string): Promise<string> {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 1, 0));\n\t\tif (placeholder) {\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", `e.g., ${placeholder}`), 1, 0));\n\t\t}\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\t`(${keyHint(\"tui.select.cancel\", \"to cancel,\")} ${keyHint(\"tui.select.confirm\", \"to submit\")})`,\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.input.setValue(\"\");\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/** Show informational text before another login step. */\n\tshowDetails(lines: string[]): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tfor (const line of lines) {\n\t\t\tthis.contentContainer.addChild(new Text(line, 1, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Show provider-owned information and links without starting an auth callback flow. */\n\tshowInfo(message: string, links: readonly AuthInfoLink[] = [], showCloseHint = false): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 1, 0));\n\t\tfor (const link of links) {\n\t\t\tconst text = link.label ? `${link.label}: ${link.url}` : link.url;\n\t\t\tconst hyperlink = `\\x1b]8;;${link.url}\\x07${text}\\x1b]8;;\\x07`;\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", hyperlink), 1, 0));\n\t\t}\n\t\tif (showCloseHint) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to close\")})`, 1, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show waiting message (for polling flows like GitHub Copilot)\n\t */\n\tshowWaiting(message: string): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 1, 0));\n\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to cancel\")})`, 1, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onProgress callback\n\t */\n\tshowProgress(message: string): void {\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 1, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to input\n\t\tthis.input.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"login-dialog.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/login-dialog.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAkB,cAAc,EAAc,MAAM,EAAE,IAAI,EAAY,MAAM,wBAAwB,CAAC;AACvH,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEpE;;GAEG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAC1C,gBAAgB,CAAY;IAC5B,KAAK,CAAQ;IACb,GAAG,CAAM;IACT,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IACxC,aAAa,CAA2B;IACxC,aAAa,CAA0B;IACvC,UAAU,CAA+C;IAEjE,2EAA2E;IACnE,QAAQ,GAAG,KAAK,CAAC;IACzB,IAAI,OAAO,GAAY;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC;IAAA,CACrB;IACD,IAAI,OAAO,CAAC,KAAc,EAAE;QAC3B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;IAAA,CAC3B;IAED,YACC,GAAQ,EACR,UAAkB,EAClB,UAAwD,EACxD,oBAA6B,EAC7B,aAAsB,EACrB;QACD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAE7B,MAAM,YAAY,GAAG,oBAAoB,IAAI,UAAU,CAAC;QACxD,MAAM,KAAK,GAAG,aAAa,IAAI,YAAY,YAAY,EAAE,CAAC;QAE1D,aAAa;QACb,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;QAEnC,QAAQ;QACR,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAErE,uBAAuB;QACvB,IAAI,CAAC,gBAAgB,GAAG,IAAI,SAAS,EAAE,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAErC,2CAA2C;QAC3C,IAAI,CAAC,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC;YAC3B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACpC,IAAI,CAAC,6BAA6B,CAAC,KAAK,CAAC,CAAC;gBAC1C,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;gBAC1B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;gBAC/B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;YAChC,CAAC;QAAA,CACD,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;QAAA,CACd,CAAC;QAEF,gBAAgB;QAChB,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;IAAA,CACnC;IAED,IAAI,MAAM,GAAgB;QACzB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;IAAA,CACnC;IAEO,6BAA6B,CAAC,KAAa,EAAQ;QAC1D,IAAI,CAAC,gBAAgB,CAAC,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7E,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAC/E,CAAC;IAAA,CACF;IAEO,MAAM,GAAS;QACtB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC;YACjD,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;YAC/B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC;IAAA,CAC1C;IAED;;OAEG;IACH,QAAQ,CAAC,GAAW,EAAE,YAAqB,EAAQ;QAClD,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC;QACzD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE9E,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,oBAAoB,CAAC;QAC7F,MAAM,SAAS,GAAG,WAAW,GAAG,OAAO,SAAS,cAAc,CAAC;QAC/D,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE3E,IAAI,YAAY,EAAE,CAAC;YAClB,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnF,CAAC;QAED,WAAW,CAAC,GAAG,CAAC,CAAC;QACjB,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAAA,CACzB;IAED;;OAEG;IACH,cAAc,CAAC,IAAyB,EAAQ;QAC/C,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,WAAW,IAAI,CAAC,eAAe,OAAO,IAAI,CAAC,eAAe,cAAc,CAAC;QAC3F,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE9E,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,oBAAoB,CAAC;QAC7F,MAAM,SAAS,GAAG,WAAW,IAAI,CAAC,eAAe,OAAO,SAAS,cAAc,CAAC;QAChF,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3E,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAEpG,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAAA,CACzB;IAED;;OAEG;IACH,eAAe,CAAC,MAAc,EAAmB;QAChD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACxB,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACxE,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3F,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QAEzB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YACvC,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;YAC7B,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC;QAAA,CAC5B,CAAC,CAAC;IAAA,CACH;IAED;;;OAGG;IACH,UAAU,CAAC,OAAe,EAAE,WAAoB,EAAmB;QAClE,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,WAAW,EAAE,CAAC;YACjB,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAC7B,IAAI,IAAI,CACP,OAAO,CAAC;YACP,CAAC,oBAAoB,EAAE,QAAQ,CAAC;YAChC,CAAC,mBAAmB,EAAE,QAAQ,CAAC;SAC/B,CAAC,EACF,CAAC,EACD,CAAC,CACD,CACD,CAAC;QAEF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QAEzB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YACvC,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;YAC7B,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC;QAAA,CAC5B,CAAC,CAAC;IAAA,CACH;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAe,EAAQ;QAClC,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,wFAAwF;IACxF,QAAQ,CAAC,OAAe,EAAE,KAAK,GAA4B,EAAE,EAAE,aAAa,GAAG,KAAK,EAAQ;QAC3F,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAClE,MAAM,SAAS,GAAG,WAAW,IAAI,CAAC,GAAG,OAAO,IAAI,cAAc,CAAC;YAC/D,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,aAAa,EAAE,CAAC;YACnB,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAAA,CACzB;IAED;;OAEG;IACH,WAAW,CAAC,OAAe,EAAQ;QAClC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzE,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3F,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAAA,CACzB;IAED;;OAEG;IACH,YAAY,CAAC,OAAe,EAAQ;QACnC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;QAE5B,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,mBAAmB,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO;QACR,CAAC;QAED,gBAAgB;QAChB,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CAC7B;CACD","sourcesContent":["import type { AuthInfoLink, OAuthDeviceCodeInfo } from \"@earendil-works/pi-ai\";\nimport { Container, type Focusable, getKeybindings, type Input, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport { openBrowser } from \"../../../utils/open-browser.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { hintRow } from \"./keybinding-hints.ts\";\nimport { PromptInput, withComposerPrompt } from \"./prompt-input.ts\";\n\n/**\n * Login dialog component - replaces editor during OAuth login flow\n */\nexport class LoginDialogComponent extends Container implements Focusable {\n\tprivate contentContainer: Container;\n\tprivate input: Input;\n\tprivate tui: TUI;\n\tprivate abortController = new AbortController();\n\tprivate inputResolver?: (value: string) => void;\n\tprivate inputRejecter?: (error: Error) => void;\n\tprivate onComplete: (success: boolean, message?: string) => void;\n\n\t// Focusable implementation - propagate to input for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tproviderId: string,\n\t\tonComplete: (success: boolean, message?: string) => void,\n\t\tproviderNameOverride?: string,\n\t\ttitleOverride?: string,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.onComplete = onComplete;\n\n\t\tconst providerName = providerNameOverride || providerId;\n\t\tconst title = titleOverride ?? `Login to ${providerName}`;\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(title)), 0, 0));\n\n\t\t// Dynamic content area\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Input (always present, used when needed)\n\t\tthis.input = new PromptInput();\n\t\tthis.input.onSubmit = () => {\n\t\t\tif (this.inputResolver) {\n\t\t\t\tconst value = this.input.getValue();\n\t\t\t\tthis.replaceInputWithSubmittedText(value);\n\t\t\t\tthis.inputResolver(value);\n\t\t\t\tthis.inputResolver = undefined;\n\t\t\t\tthis.inputRejecter = undefined;\n\t\t\t}\n\t\t};\n\t\tthis.input.onEscape = () => {\n\t\t\tthis.cancel();\n\t\t};\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\tprivate replaceInputWithSubmittedText(value: string): void {\n\t\tthis.contentContainer.children = this.contentContainer.children.map((child) =>\n\t\t\tchild === this.input ? new Text(withComposerPrompt(`> ${value}`), 0, 0) : child,\n\t\t);\n\t}\n\n\tprivate cancel(): void {\n\t\tthis.abortController.abort();\n\t\tif (this.inputRejecter) {\n\t\t\tthis.inputRejecter(new Error(\"Login cancelled\"));\n\t\t\tthis.inputResolver = undefined;\n\t\t\tthis.inputRejecter = undefined;\n\t\t}\n\t\tthis.onComplete(false, \"Login cancelled\");\n\t}\n\n\t/**\n\t * Called by onAuth callback - show URL and optional instructions\n\t */\n\tshowAuth(url: string, instructions?: string): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${url}\\x07${url}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 0, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${url}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 0, 0));\n\n\t\tif (instructions) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", instructions), 0, 0));\n\t\t}\n\n\t\topenBrowser(url);\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onDeviceCode callback - show URL and user code.\n\t */\n\tshowDeviceCode(info: OAuthDeviceCodeInfo): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${info.verificationUri}\\x07${info.verificationUri}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 0, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${info.verificationUri}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 0, 0));\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", `Enter code: ${info.userCode}`), 0, 0));\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show input for manual code/URL entry (for callback server providers)\n\t */\n\tshowManualInput(prompt: string): Promise<string> {\n\t\tthis.input.setValue(\"\");\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", prompt), 0, 0));\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"cancel\"]]), 0, 0));\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/**\n\t * Called by onPrompt callback - show prompt and wait for input\n\t * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)\n\t */\n\tshowPrompt(message: string, placeholder?: string): Promise<string> {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 0, 0));\n\t\tif (placeholder) {\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", `e.g., ${placeholder}`), 0, 0));\n\t\t}\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\thintRow([\n\t\t\t\t\t[\"tui.select.confirm\", \"submit\"],\n\t\t\t\t\t[\"tui.select.cancel\", \"cancel\"],\n\t\t\t\t]),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.input.setValue(\"\");\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/** Show informational text before another login step. */\n\tshowDetails(lines: string[]): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tfor (const line of lines) {\n\t\t\tthis.contentContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Show provider-owned information and links without starting an auth callback flow. */\n\tshowInfo(message: string, links: readonly AuthInfoLink[] = [], showCloseHint = false): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 0, 0));\n\t\tfor (const link of links) {\n\t\t\tconst text = link.label ? `${link.label}: ${link.url}` : link.url;\n\t\t\tconst hyperlink = `\\x1b]8;;${link.url}\\x07${text}\\x1b]8;;\\x07`;\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", hyperlink), 0, 0));\n\t\t}\n\t\tif (showCloseHint) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"close\"]]), 0, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show waiting message (for polling flows like GitHub Copilot)\n\t */\n\tshowWaiting(message: string): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 0, 0));\n\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"cancel\"]]), 0, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onProgress callback\n\t */\n\tshowProgress(message: string): void {\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 0, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to input\n\t\tthis.input.handleInput(data);\n\t}\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { type Model, type ModelThinkingLevel } from "@earendil-works/pi-ai";
2
- import { Container, type Focusable, Input, type TUI } from "@earendil-works/pi-tui";
2
+ import { Container, type Focusable, type Input, type TUI } from "@earendil-works/pi-tui";
3
3
  import type { ModelRuntime } from "../../../core/model-runtime.ts";
4
4
  interface ScopedModelItem {
5
5
  model: Model<any>;
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tclampThinkingLevel,\n\tgetSupportedThinkingLevels,\n\ttype Model,\n\ttype ModelThinkingLevel,\n\tmodelsAreEqual,\n} from \"@earendil-works/pi-ai\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tfuzzyFilter,\n\tgetKeybindings,\n\tInput,\n\tKey,\n\tmatchesKey,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@earendil-works/pi-tui\";\nimport type { ModelRuntime } from \"../../../core/model-runtime.ts\";\nimport { refreshModelCatalogs } from \"../model-catalog-refresh.ts\";\nimport { getModelSelectorSearchText } from \"../model-search.ts\";\nimport { paintSelectedRow, paintSelectedRowToWidth, theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, keyText, rawKeyHint } from \"./keybinding-hints.ts\";\nimport {\n\tcomposeModelRow,\n\ttype EffortLayout,\n\ttype EffortLayoutRow,\n\tgetEffortLayout,\n\tNO_EFFORT_CLUSTER,\n\trenderPricePanel,\n\ttype TrailingSegment,\n} from \"./model-row.ts\";\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\ninterface DefaultModelReference {\n\tprovider: string;\n\tid: string;\n}\n\ntype ModelScope = \"all\" | \"scoped\";\n\n/**\n * Which of the two steps the selector is on. `provider: null` lists every provider's models at once,\n * which is how a `/model <search>` invocation and a single-provider setup both open.\n */\ntype Step = { kind: \"providers\" } | { kind: \"models\"; provider: string | null };\n\ntype Row =\n\t| { kind: \"provider\"; provider: string; count: number; hasCurrent: boolean }\n\t| { kind: \"model\"; item: ModelItem };\n\nconst MAX_VISIBLE_ROWS = 10;\n\n/**\n * Component that renders a two-step model selector: provider first, then that provider's models.\n */\nexport class ModelSelectorComponent extends Container implements Focusable {\n\tprivate searchInput: Input;\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\tprivate headerContainer: Container;\n\tprivate listContainer: Container;\n\tprivate footerContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate scopedModelItems: ModelItem[] = [];\n\tprivate activeModels: ModelItem[] = [];\n\tprivate rows: Row[] = [];\n\tprivate filteredRows: Row[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel?: Model<any>;\n\tprivate modelRuntime: ModelRuntime;\n\tprivate onSelectCallback: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void;\n\tprivate onSelectAsDefaultCallback?: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage?: string;\n\tprivate refreshStatusMessage = \"Refreshing model catalogs…\";\n\tprivate refreshStatusSuccess = false;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\tprivate defaultModel?: DefaultModelReference;\n\tprivate scope: ModelScope = \"all\";\n\tprivate step: Step = { kind: \"providers\" };\n\tprivate readonly refreshAbortController = new AbortController();\n\tprivate refreshTimeout?: ReturnType<typeof setTimeout>;\n\tprivate closed = false;\n\t/** Effort dialled per model in this session of the picker, keyed by `provider/id`. */\n\tprivate readonly effortLevels = new Map<string, ModelThinkingLevel>();\n\t/** Models whose effort the user actually moved. Only those override the session level. */\n\tprivate readonly editedEffortModels = new Set<string>();\n\tprivate readonly initialThinkingLevel?: ModelThinkingLevel;\n\t/**\n\t * True once up or down has moved the selection into the list. Until then\n\t * left and right belong to the search field's cursor, which is where they\n\t * are needed while a query is still being typed.\n\t */\n\tprivate navigatedIntoList = false;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | undefined,\n\t\tmodelRuntime: ModelRuntime,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\n\t\tonSelect: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void,\n\t\tonCancel: () => void,\n\t\tinitialSearchInput?: string,\n\t\tonSelectAsDefault?: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void,\n\t\tdefaultModel?: DefaultModelReference,\n\t\toptions: { thinkingLevel?: ModelThinkingLevel } = {},\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.initialThinkingLevel = options.thinkingLevel;\n\t\tthis.currentModel = currentModel;\n\t\tthis.modelRuntime = modelRuntime;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.defaultModel = defaultModel;\n\t\tthis.scope = scopedModels.length > 0 ? \"scoped\" : \"all\";\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onSelectAsDefaultCallback = onSelectAsDefault;\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\tthis.headerContainer = new Container();\n\t\tthis.addChild(this.headerContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tif (initialSearchInput) {\n\t\t\tthis.searchInput.setValue(initialSearchInput);\n\t\t}\n\t\tthis.searchInput.onSubmit = () => this.confirmSelection();\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// Keybindings live under the list, where the eye already is once it has\n\t\t// finished reading. Above the list they were the first thing read and the\n\t\t// least useful.\n\t\tthis.footerContainer = new Container();\n\t\tthis.addChild(this.footerContainer);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Render the current snapshot immediately, then refresh in the background.\n\t\tthis.loadModelsFromSnapshot();\n\t\tthis.step = this.initialStep(initialSearchInput);\n\t\tthis.rebuildRows();\n\t\tthis.selectedIndex = this.defaultSelectedIndex();\n\t\tthis.renderHeader();\n\t\tthis.filterRows(this.searchInput.getValue());\n\t\tthis.tui.requestRender();\n\t\tvoid this.refreshModels();\n\t}\n\n\tprivate initialStep(initialSearchInput: string | undefined): Step {\n\t\tconst providers = this.activeProviders();\n\t\tif (initialSearchInput) return { kind: \"models\", provider: null };\n\t\tif (providers.length <= 1) return { kind: \"models\", provider: providers[0] ?? null };\n\t\treturn { kind: \"providers\" };\n\t}\n\n\tprivate activeProviders(): string[] {\n\t\treturn [...new Set(this.activeModels.map((item) => item.provider))];\n\t}\n\n\tprivate loadModelsFromSnapshot(): void {\n\t\tconst models = this.modelRuntime.getAvailableSnapshot().map((model: Model<any>) => ({\n\t\t\tprovider: model.provider,\n\t\t\tid: model.id,\n\t\t\tmodel,\n\t\t}));\n\t\tthis.allModels = this.sortModels(models);\n\t\tthis.scopedModels = this.scopedModels.map((scoped) => {\n\t\t\tconst refreshed = this.modelRuntime.getModel(scoped.model.provider, scoped.model.id);\n\t\t\treturn refreshed ? { ...scoped, model: refreshed } : scoped;\n\t\t});\n\t\tthis.scopedModelItems = this.scopedModels.map((scoped) => ({\n\t\t\tprovider: scoped.model.provider,\n\t\t\tid: scoped.model.id,\n\t\t\tmodel: scoped.model,\n\t\t}));\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t}\n\n\tprivate async refreshModels(): Promise<void> {\n\t\tconst timeoutMs = 15_000;\n\t\tlet timedOut = false;\n\t\tthis.refreshTimeout = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tthis.refreshAbortController.abort();\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tconst result = await refreshModelCatalogs(this.modelRuntime, this.refreshAbortController.signal);\n\t\t\tif (this.closed) return;\n\t\t\tthis.refreshStatusMessage = \"\";\n\t\t\tif (result.aborted && timedOut) {\n\t\t\t\tthis.errorMessage = \"Model refresh timed out; showing cached models.\";\n\t\t\t} else if (result.errors.size === 1) {\n\t\t\t\tthis.errorMessage = `Could not refresh ${result.errors.keys().next().value}; showing cached models.`;\n\t\t\t} else if (result.errors.size > 1) {\n\t\t\t\tthis.errorMessage = `Could not refresh ${result.errors.size} model catalogs (${[...result.errors.keys()].join(\", \")}); showing cached models.`;\n\t\t\t} else {\n\t\t\t\tthis.errorMessage = this.modelRuntime.getError();\n\t\t\t\tif (!this.errorMessage) {\n\t\t\t\t\tthis.refreshStatusMessage = \"Model catalogs refreshed.\";\n\t\t\t\t\tthis.refreshStatusSuccess = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.loadModelsFromSnapshot();\n\t\t\tthis.rebuildRows();\n\t\t\tthis.renderHeader();\n\t\t\tthis.filterRows(this.searchInput.getValue());\n\t\t\tthis.tui.requestRender();\n\t\t} catch (error) {\n\t\t\tif (this.closed) return;\n\t\t\tthis.refreshStatusMessage = \"\";\n\t\t\tthis.errorMessage = timedOut\n\t\t\t\t? \"Model refresh timed out; showing cached models.\"\n\t\t\t\t: `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`;\n\t\t\tthis.updateList();\n\t\t\tthis.tui.requestRender();\n\t\t} finally {\n\t\t\tif (this.refreshTimeout) clearTimeout(this.refreshTimeout);\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tif (this.refreshTimeout) clearTimeout(this.refreshTimeout);\n\t\tthis.refreshAbortController.abort();\n\t}\n\n\tprivate sortModels(models: ModelItem[]): ModelItem[] {\n\t\tconst sorted = [...models];\n\t\t// Sort: current model first, default model second, then by provider.\n\t\tsorted.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\tconst aIsDefault = this.isDefaultModel(a.model);\n\t\t\tconst bIsDefault = this.isDefaultModel(b.model);\n\t\t\tif (aIsDefault && !bIsDefault) return -1;\n\t\t\tif (!aIsDefault && bIsDefault) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\t\treturn sorted;\n\t}\n\n\tprivate rebuildRows(): void {\n\t\tif (this.step.kind === \"providers\") {\n\t\t\tconst byProvider = new Map<string, ModelItem[]>();\n\t\t\tfor (const item of this.activeModels) {\n\t\t\t\tconst existing = byProvider.get(item.provider);\n\t\t\t\tif (existing) existing.push(item);\n\t\t\t\telse byProvider.set(item.provider, [item]);\n\t\t\t}\n\t\t\tconst providerRows: Row[] = [...byProvider].map(([provider, items]) => ({\n\t\t\t\tkind: \"provider\",\n\t\t\t\tprovider,\n\t\t\t\tcount: items.length,\n\t\t\t\thasCurrent: items.some((item) => modelsAreEqual(this.currentModel, item.model)),\n\t\t\t}));\n\t\t\tproviderRows.sort((a, b) => {\n\t\t\t\tif (a.kind !== \"provider\" || b.kind !== \"provider\") return 0;\n\t\t\t\tif (a.hasCurrent !== b.hasCurrent) return a.hasCurrent ? -1 : 1;\n\t\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t\t});\n\t\t\tthis.rows = providerRows;\n\t\t\treturn;\n\t\t}\n\t\tconst provider = this.step.provider;\n\t\tconst models = provider === null ? this.activeModels : this.activeModels.filter((m) => m.provider === provider);\n\t\tthis.rows = models.map((item) => ({ kind: \"model\", item }));\n\t}\n\n\tprivate defaultSelectedIndex(): number {\n\t\tconst index = this.rows.findIndex((row) =>\n\t\t\trow.kind === \"provider\" ? row.hasCurrent : modelsAreEqual(this.currentModel, row.item.model),\n\t\t);\n\t\treturn index >= 0 ? index : 0;\n\t}\n\n\tprivate setStep(step: Step): void {\n\t\tthis.step = step;\n\t\tthis.navigatedIntoList = false;\n\t\tthis.searchInput.setValue(\"\");\n\t\tthis.rebuildRows();\n\t\tthis.selectedIndex = this.defaultSelectedIndex();\n\t\tthis.renderHeader();\n\t\tthis.filterRows(\"\");\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate isDrilledIn(): boolean {\n\t\treturn this.step.kind === \"models\" && this.step.provider !== null;\n\t}\n\n\t/** Escape returns to the provider list whenever there is a provider list worth returning to. */\n\tprivate canGoBack(): boolean {\n\t\treturn this.step.kind === \"models\" && this.activeProviders().length > 1;\n\t}\n\n\tprivate renderHeader(): void {\n\t\tthis.headerContainer.clear();\n\n\t\tif (this.step.kind === \"providers\") {\n\t\t\tthis.headerContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Select a provider\")), 0, 0));\n\t\t} else {\n\t\t\tconst label = this.step.provider ?? \"All providers\";\n\t\t\tconst crumb = `${theme.bold(theme.fg(\"accent\", label))}${theme.fg(\"muted\", \" › select a model\")}`;\n\t\t\tthis.headerContainer.addChild(new Text(crumb, 0, 0));\n\t\t}\n\n\t\tif (this.scopedModelItems.length > 0) {\n\t\t\tthis.headerContainer.addChild(new Text(this.getScopeText(), 0, 0));\n\t\t} else if (!this.isDrilledIn()) {\n\t\t\tconst hintText = \"Only showing models from configured providers. Use /login to add providers.\";\n\t\t\tthis.headerContainer.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\n\t\t}\n\n\t\tthis.renderFooter();\n\t}\n\n\tprivate renderFooter(): void {\n\t\tthis.footerContainer.clear();\n\t\tthis.footerContainer.addChild(new Text(this.getHintText(), 0, 0));\n\t}\n\n\tprivate getScopeText(): string {\n\t\tconst allText = this.scope === \"all\" ? theme.fg(\"accent\", \"all\") : theme.fg(\"muted\", \"all\");\n\t\tconst scopedText = this.scope === \"scoped\" ? theme.fg(\"accent\", \"scoped\") : theme.fg(\"muted\", \"scoped\");\n\t\treturn `${theme.fg(\"muted\", \"Scope: \")}${allText}${theme.fg(\"muted\", \" | \")}${scopedText}`;\n\t}\n\n\tprivate getHintText(): string {\n\t\tconst hints: string[] = [\n\t\t\t// Both arrows, since either one moves the selection.\n\t\t\trawKeyHint(`${keyText(\"tui.select.up\")}/${keyText(\"tui.select.down\")}`, \"move\"),\n\t\t\tkeyHint(\"tui.select.confirm\", \"select\"),\n\t\t];\n\t\tif (this.hasAdjustableEffort()) {\n\t\t\tconst left = keyText(\"tui.editor.cursorLeft\", { primaryOnly: true });\n\t\t\tconst right = keyText(\"tui.editor.cursorRight\", { primaryOnly: true });\n\t\t\thints.push(rawKeyHint(`${left}/${right}`, \"effort\"));\n\t\t}\n\t\tif (this.scopedModelItems.length > 0) {\n\t\t\thints.push(keyHint(\"tui.input.tab\", \"scope\") + theme.fg(\"muted\", \" (all/scoped)\"));\n\t\t}\n\t\tif (this.onSelectAsDefaultCallback) hints.push(rawKeyHint(\"ctrl+s\", \"set as default\"));\n\t\tif (this.canGoBack()) hints.push(rawKeyHint(\"escape\", \"providers\"), rawKeyHint(\"ctrl+c\", \"close\"));\n\t\telse hints.push(keyHint(\"tui.select.cancel\", \"close\"));\n\t\treturn hints.join(theme.fg(\"borderMuted\", \" · \"));\n\t}\n\n\t/** Levels worth dialling. A model with nothing but \"off\" has no cluster to show. */\n\tprivate getSelectableLevels(item: ModelItem): ModelThinkingLevel[] {\n\t\tconst levels = getSupportedThinkingLevels(item.model);\n\t\tif (levels.length === 1 && levels[0] === \"off\") return [];\n\t\treturn levels;\n\t}\n\n\tprivate getEffort(item: ModelItem): ModelThinkingLevel | undefined {\n\t\tconst levels = this.getSelectableLevels(item);\n\t\tif (levels.length === 0) return undefined;\n\t\tconst key = `${item.provider}/${item.id}`;\n\t\tconst stored = this.effortLevels.get(key);\n\t\tif (stored !== undefined && levels.includes(stored)) return stored;\n\t\tconst requested = this.initialThinkingLevel ?? \"off\";\n\t\tconst clamped = levels.includes(requested) ? requested : clampThinkingLevel(item.model, requested);\n\t\tconst resolved = levels.includes(clamped) ? clamped : levels[0]!;\n\t\tthis.effortLevels.set(key, resolved);\n\t\treturn resolved;\n\t}\n\n\tprivate adjustEffort(item: ModelItem, direction: number): boolean {\n\t\tconst levels = this.getSelectableLevels(item);\n\t\tif (levels.length === 0) return false;\n\t\tconst key = `${item.provider}/${item.id}`;\n\t\tconst current = this.getEffort(item) ?? levels[0]!;\n\t\tconst next = levels[(levels.indexOf(current) + direction + levels.length) % levels.length]!;\n\t\tthis.effortLevels.set(key, next);\n\t\tthis.editedEffortModels.add(key);\n\t\treturn true;\n\t}\n\n\t/** The effort to hand back on select, or undefined when the user left it alone. */\n\tprivate getChosenEffort(item: ModelItem): ModelThinkingLevel | undefined {\n\t\treturn this.editedEffortModels.has(`${item.provider}/${item.id}`) ? this.getEffort(item) : undefined;\n\t}\n\n\tprivate hasAdjustableEffort(): boolean {\n\t\treturn this.filteredRows.some((row) => row.kind === \"model\" && this.getSelectableLevels(row.item).length > 0);\n\t}\n\n\t/**\n\t * Context that trails a model row, right-aligned, in reading order.\n\t *\n\t * Each segment carries what it costs to lose, because a narrow row drops by\n\t * that and not by position. `current` outranks the id: the row already shows\n\t * the model's name, so the id is largely recoverable from it, while nothing\n\t * else on screen says which model the session is on. The provider badge goes\n\t * first, being the least you lose.\n\t */\n\tprivate getTrailingSegments(item: ModelItem): TrailingSegment[] {\n\t\tconst segments: TrailingSegment[] = [];\n\t\tif (this.step.kind === \"models\" && this.step.provider === null) {\n\t\t\tsegments.push({ text: theme.fg(\"muted\", item.provider), priority: 0 });\n\t\t}\n\t\tsegments.push({ text: theme.fg(\"muted\", item.id), priority: 2 });\n\t\tif (this.isDefaultModel(item.model)) segments.push({ text: theme.fg(\"muted\", \"default\"), priority: 1 });\n\t\tif (modelsAreEqual(this.currentModel, item.model)) {\n\t\t\tsegments.push({ text: theme.fg(\"success\", \"current\"), priority: 3 });\n\t\t}\n\t\treturn segments;\n\t}\n\n\tprivate isDefaultModel(model: Model<any>): boolean {\n\t\treturn this.defaultModel?.provider === model.provider && this.defaultModel.id === model.id;\n\t}\n\n\tprivate isDefaultSearch(query: string): boolean {\n\t\tconst normalized = query.trim().toLowerCase();\n\t\treturn normalized.length > 0 && \"default\".startsWith(normalized);\n\t}\n\n\tprivate setScope(scope: ModelScope): void {\n\t\tif (this.scope === scope) return;\n\t\tthis.scope = scope;\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tthis.rebuildRows();\n\t\tthis.selectedIndex = this.defaultSelectedIndex();\n\t\tthis.renderHeader();\n\t\tthis.filterRows(this.searchInput.getValue());\n\t}\n\n\tprivate rowSearchText(row: Row): string {\n\t\tif (row.kind === \"provider\") return row.provider;\n\t\tconst defaultText = this.isDefaultModel(row.item.model) ? \" default\" : \"\";\n\t\treturn `${getModelSelectorSearchText({ id: row.item.id, provider: row.item.provider, name: row.item.model.name })}${defaultText}`;\n\t}\n\n\tprivate isDefaultRow(row: Row): boolean {\n\t\treturn row.kind === \"model\" && this.isDefaultModel(row.item.model);\n\t}\n\n\tprivate filterRows(query: string): void {\n\t\tif (!query) {\n\t\t\tthis.filteredRows = this.rows;\n\t\t} else {\n\t\t\tconst filtered = fuzzyFilter(this.rows, query, (row) => this.rowSearchText(row));\n\t\t\tif (this.isDefaultSearch(query)) {\n\t\t\t\t// Typing \"default\" (or a prefix of it) surfaces the default model even\n\t\t\t\t// when its name doesn't otherwise fuzzy-match the query.\n\t\t\t\tconst defaultRows = this.rows.filter((row) => this.isDefaultRow(row));\n\t\t\t\tthis.filteredRows = [...defaultRows, ...filtered.filter((row) => !this.isDefaultRow(row))];\n\t\t\t} else {\n\t\t\t\tthis.filteredRows = filtered;\n\t\t\t}\n\t\t}\n\t\t// When filtering by a query, move the selector to the top row so the best\n\t\t// match is highlighted. When the query is cleared, keep the current position\n\t\t// clamped to the (restored) list length.\n\t\tthis.selectedIndex = query ? 0 : Math.min(this.selectedIndex, Math.max(0, this.filteredRows.length - 1));\n\t\t// The effort hint depends on what survived the filter, so the footer is\n\t\t// rebuilt here rather than with the header, which runs before filtering.\n\t\tthis.renderFooter();\n\t\tthis.updateList();\n\t}\n\n\tprivate renderProviderRow(row: Extract<Row, { kind: \"provider\" }>, isSelected: boolean): string {\n\t\tconst prefix = isSelected ? \"→ \" : \" \";\n\t\tconst count = theme.fg(\"muted\", ` (${row.count} model${row.count === 1 ? \"\" : \"s\"})`);\n\t\tconst checkmark = row.hasCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\tconst composed = `${prefix}${row.provider}${count}${checkmark}`;\n\t\treturn isSelected ? paintSelectedRow(composed) : composed;\n\t}\n\n\t/**\n\t * The visible page of rows, rendered together because the effort cluster is\n\t * placed once for the whole page rather than per row.\n\t */\n\tprivate createRowsComponent(startIndex: number, endIndex: number): Component {\n\t\tconst rows = this.filteredRows.slice(startIndex, endIndex);\n\t\treturn {\n\t\t\trender: (width: number): string[] => {\n\t\t\t\tconst layout = this.pageEffortLayout(rows, width);\n\t\t\t\treturn rows.map((row, offset) => {\n\t\t\t\t\tconst isSelected = startIndex + offset === this.selectedIndex;\n\t\t\t\t\tif (row.kind === \"provider\") return this.renderProviderRow(row, isSelected);\n\t\t\t\t\tconst composed = composeModelRow({\n\t\t\t\t\t\tname: row.item.model.name,\n\t\t\t\t\t\tlevels: this.getSelectableLevels(row.item),\n\t\t\t\t\t\teffort: this.getEffort(row.item),\n\t\t\t\t\t\ttrailingSegments: this.getTrailingSegments(row.item),\n\t\t\t\t\t\tselected: isSelected,\n\t\t\t\t\t\tlayout,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t});\n\t\t\t\t\treturn isSelected ? paintSelectedRowToWidth(composed, width) : composed;\n\t\t\t\t});\n\t\t\t},\n\t\t\tinvalidate: () => {},\n\t\t};\n\t}\n\n\tprivate pageEffortLayout(rows: ReadonlyArray<Row>, width: number): EffortLayout {\n\t\tconst layoutRows: EffortLayoutRow[] = [];\n\t\tfor (const row of rows) {\n\t\t\tif (row.kind !== \"model\") continue;\n\t\t\tlayoutRows.push({\n\t\t\t\tname: row.item.model.name,\n\t\t\t\tlevels: this.getSelectableLevels(row.item),\n\t\t\t\ttrailingSegments: this.getTrailingSegments(row.item),\n\t\t\t});\n\t\t}\n\t\treturn layoutRows.length > 0 ? getEffortLayout(layoutRows, width) : NO_EFFORT_CLUSTER;\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(MAX_VISIBLE_ROWS / 2), this.filteredRows.length - MAX_VISIBLE_ROWS),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + MAX_VISIBLE_ROWS, this.filteredRows.length);\n\n\t\tif (endIndex > startIndex) {\n\t\t\tthis.listContainer.addChild(this.createRowsComponent(startIndex, endIndex));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredRows.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredRows.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.filteredRows.length === 0) {\n\t\t\tconst empty = this.step.kind === \"providers\" ? \" No matching providers\" : \" No matching models\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", empty), 0, 0));\n\t\t} else {\n\t\t\tconst selected = this.filteredRows[this.selectedIndex];\n\t\t\tif (selected?.kind === \"model\") {\n\t\t\t\tconst cost = selected.item.model.cost;\n\t\t\t\tthis.listContainer.addChild({\n\t\t\t\t\trender: (width: number) => renderPricePanel(cost, width),\n\t\t\t\t\tinvalidate: () => {},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (this.refreshStatusMessage) {\n\t\t\tthis.listContainer.addChild(new Spacer(1));\n\t\t\tthis.listContainer.addChild(\n\t\t\t\tnew Text(theme.fg(this.refreshStatusSuccess ? \"success\" : \"muted\", ` ${this.refreshStatusMessage}`), 0, 0),\n\t\t\t);\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tif (this.scopedModelItems.length > 0) {\n\t\t\t\tthis.setScope(this.scope === \"all\" ? \"scoped\" : \"all\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Left/right dial the highlighted model's effort, but only once they are\n\t\t// free to: while a query is being typed they still drive the search cursor.\n\t\tif (\n\t\t\t(this.searchInput.getValue() === \"\" || this.navigatedIntoList) &&\n\t\t\t(kb.matches(keyData, \"tui.editor.cursorLeft\") || kb.matches(keyData, \"tui.editor.cursorRight\"))\n\t\t) {\n\t\t\tconst row = this.filteredRows[this.selectedIndex];\n\t\t\tconst direction = kb.matches(keyData, \"tui.editor.cursorLeft\") ? -1 : 1;\n\t\t\tif (row?.kind === \"model\" && this.adjustEffort(row.item, direction)) {\n\t\t\t\tthis.updateList();\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filteredRows.length === 0) return;\n\t\t\tthis.navigatedIntoList = true;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredRows.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 (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filteredRows.length === 0) return;\n\t\t\tthis.navigatedIntoList = true;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredRows.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tthis.confirmSelection();\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\t// Escape steps back through the two levels; Ctrl+C always closes.\n\t\t\tif (matchesKey(keyData, Key.escape) && this.canGoBack()) {\n\t\t\t\tthis.setStep({ kind: \"providers\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.dispose();\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Ctrl+S — select and save as default\n\t\telse if (matchesKey(keyData, \"ctrl+s\") && this.onSelectAsDefaultCallback) {\n\t\t\tconst row = this.filteredRows[this.selectedIndex];\n\t\t\tif (row?.kind === \"model\") {\n\t\t\t\tthis.dispose();\n\t\t\t\tthis.onSelectAsDefaultCallback(row.item.model, this.getChosenEffort(row.item));\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tconst previousQuery = this.searchInput.getValue();\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tif (previousQuery !== this.searchInput.getValue()) {\n\t\t\t\tthis.navigatedIntoList = false;\n\t\t\t}\n\t\t\tthis.filterRows(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate confirmSelection(): void {\n\t\tconst row = this.filteredRows[this.selectedIndex];\n\t\tif (!row) return;\n\t\tif (row.kind === \"provider\") {\n\t\t\tthis.setStep({ kind: \"models\", provider: row.provider });\n\t\t\treturn;\n\t\t}\n\t\tthis.dispose();\n\t\tthis.onSelectCallback(row.item.model, this.getChosenEffort(row.item));\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":"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","sourcesContent":["import {\n\tclampThinkingLevel,\n\tgetSupportedThinkingLevels,\n\ttype Model,\n\ttype ModelThinkingLevel,\n\tmodelsAreEqual,\n} from \"@earendil-works/pi-ai\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tfuzzyFilter,\n\tgetKeybindings,\n\ttype Input,\n\tKey,\n\tmatchesKey,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@earendil-works/pi-tui\";\nimport type { ModelRuntime } from \"../../../core/model-runtime.ts\";\nimport { refreshModelCatalogs } from \"../model-catalog-refresh.ts\";\nimport { getModelSelectorSearchText } from \"../model-search.ts\";\nimport { paintSelectedRow, paintSelectedRowToWidth, theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { type HintKey, hintRow } from \"./keybinding-hints.ts\";\nimport {\n\tcomposeModelRow,\n\ttype EffortLayout,\n\ttype EffortLayoutRow,\n\tgetEffortLayout,\n\tNO_EFFORT_CLUSTER,\n\trenderPricePanel,\n\ttype TrailingSegment,\n} from \"./model-row.ts\";\nimport { PromptInput } from \"./prompt-input.ts\";\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\ninterface DefaultModelReference {\n\tprovider: string;\n\tid: string;\n}\n\ntype ModelScope = \"all\" | \"scoped\";\n\n/**\n * Which of the two steps the selector is on. `provider: null` lists every provider's models at once,\n * which is how a `/model <search>` invocation and a single-provider setup both open.\n */\ntype Step = { kind: \"providers\" } | { kind: \"models\"; provider: string | null };\n\ntype Row =\n\t| { kind: \"provider\"; provider: string; count: number; hasCurrent: boolean }\n\t| { kind: \"model\"; item: ModelItem };\n\nconst MAX_VISIBLE_ROWS = 10;\n\n/**\n * Component that renders a two-step model selector: provider first, then that provider's models.\n */\nexport class ModelSelectorComponent extends Container implements Focusable {\n\tprivate searchInput: Input;\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\tprivate headerContainer: Container;\n\tprivate listContainer: Container;\n\tprivate footerContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate scopedModelItems: ModelItem[] = [];\n\tprivate activeModels: ModelItem[] = [];\n\tprivate rows: Row[] = [];\n\tprivate filteredRows: Row[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel?: Model<any>;\n\tprivate modelRuntime: ModelRuntime;\n\tprivate onSelectCallback: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void;\n\tprivate onSelectAsDefaultCallback?: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage?: string;\n\tprivate refreshStatusMessage = \"Refreshing model catalogs…\";\n\tprivate refreshStatusSuccess = false;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\tprivate defaultModel?: DefaultModelReference;\n\tprivate scope: ModelScope = \"all\";\n\tprivate step: Step = { kind: \"providers\" };\n\tprivate readonly refreshAbortController = new AbortController();\n\tprivate refreshTimeout?: ReturnType<typeof setTimeout>;\n\tprivate closed = false;\n\t/** Effort dialled per model in this session of the picker, keyed by `provider/id`. */\n\tprivate readonly effortLevels = new Map<string, ModelThinkingLevel>();\n\t/** Models whose effort the user actually moved. Only those override the session level. */\n\tprivate readonly editedEffortModels = new Set<string>();\n\tprivate readonly initialThinkingLevel?: ModelThinkingLevel;\n\t/**\n\t * True once up or down has moved the selection into the list. Until then\n\t * left and right belong to the search field's cursor, which is where they\n\t * are needed while a query is still being typed.\n\t */\n\tprivate navigatedIntoList = false;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | undefined,\n\t\tmodelRuntime: ModelRuntime,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\n\t\tonSelect: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void,\n\t\tonCancel: () => void,\n\t\tinitialSearchInput?: string,\n\t\tonSelectAsDefault?: (model: Model<any>, thinkingLevel?: ModelThinkingLevel) => void,\n\t\tdefaultModel?: DefaultModelReference,\n\t\toptions: { thinkingLevel?: ModelThinkingLevel } = {},\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.initialThinkingLevel = options.thinkingLevel;\n\t\tthis.currentModel = currentModel;\n\t\tthis.modelRuntime = modelRuntime;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.defaultModel = defaultModel;\n\t\tthis.scope = scopedModels.length > 0 ? \"scoped\" : \"all\";\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onSelectAsDefaultCallback = onSelectAsDefault;\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\tthis.headerContainer = new Container();\n\t\tthis.addChild(this.headerContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new PromptInput();\n\t\tif (initialSearchInput) {\n\t\t\tthis.searchInput.setValue(initialSearchInput);\n\t\t}\n\t\tthis.searchInput.onSubmit = () => this.confirmSelection();\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// Keybindings live under the list, where the eye already is once it has\n\t\t// finished reading. Above the list they were the first thing read and the\n\t\t// least useful.\n\t\tthis.footerContainer = new Container();\n\t\tthis.addChild(this.footerContainer);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Render the current snapshot immediately, then refresh in the background.\n\t\tthis.loadModelsFromSnapshot();\n\t\tthis.step = this.initialStep(initialSearchInput);\n\t\tthis.rebuildRows();\n\t\tthis.selectedIndex = this.defaultSelectedIndex();\n\t\tthis.renderHeader();\n\t\tthis.filterRows(this.searchInput.getValue());\n\t\tthis.tui.requestRender();\n\t\tvoid this.refreshModels();\n\t}\n\n\tprivate initialStep(initialSearchInput: string | undefined): Step {\n\t\tconst providers = this.activeProviders();\n\t\tif (initialSearchInput) return { kind: \"models\", provider: null };\n\t\tif (providers.length <= 1) return { kind: \"models\", provider: providers[0] ?? null };\n\t\treturn { kind: \"providers\" };\n\t}\n\n\tprivate activeProviders(): string[] {\n\t\treturn [...new Set(this.activeModels.map((item) => item.provider))];\n\t}\n\n\tprivate loadModelsFromSnapshot(): void {\n\t\tconst models = this.modelRuntime.getAvailableSnapshot().map((model: Model<any>) => ({\n\t\t\tprovider: model.provider,\n\t\t\tid: model.id,\n\t\t\tmodel,\n\t\t}));\n\t\tthis.allModels = this.sortModels(models);\n\t\tthis.scopedModels = this.scopedModels.map((scoped) => {\n\t\t\tconst refreshed = this.modelRuntime.getModel(scoped.model.provider, scoped.model.id);\n\t\t\treturn refreshed ? { ...scoped, model: refreshed } : scoped;\n\t\t});\n\t\tthis.scopedModelItems = this.scopedModels.map((scoped) => ({\n\t\t\tprovider: scoped.model.provider,\n\t\t\tid: scoped.model.id,\n\t\t\tmodel: scoped.model,\n\t\t}));\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t}\n\n\tprivate async refreshModels(): Promise<void> {\n\t\tconst timeoutMs = 15_000;\n\t\tlet timedOut = false;\n\t\tthis.refreshTimeout = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tthis.refreshAbortController.abort();\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tconst result = await refreshModelCatalogs(this.modelRuntime, this.refreshAbortController.signal);\n\t\t\tif (this.closed) return;\n\t\t\tthis.refreshStatusMessage = \"\";\n\t\t\tif (result.aborted && timedOut) {\n\t\t\t\tthis.errorMessage = \"Model refresh timed out; showing cached models.\";\n\t\t\t} else if (result.errors.size === 1) {\n\t\t\t\tthis.errorMessage = `Could not refresh ${result.errors.keys().next().value}; showing cached models.`;\n\t\t\t} else if (result.errors.size > 1) {\n\t\t\t\tthis.errorMessage = `Could not refresh ${result.errors.size} model catalogs (${[...result.errors.keys()].join(\", \")}); showing cached models.`;\n\t\t\t} else {\n\t\t\t\tthis.errorMessage = this.modelRuntime.getError();\n\t\t\t\tif (!this.errorMessage) {\n\t\t\t\t\tthis.refreshStatusMessage = \"Model catalogs refreshed.\";\n\t\t\t\t\tthis.refreshStatusSuccess = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.loadModelsFromSnapshot();\n\t\t\tthis.rebuildRows();\n\t\t\tthis.renderHeader();\n\t\t\tthis.filterRows(this.searchInput.getValue());\n\t\t\tthis.tui.requestRender();\n\t\t} catch (error) {\n\t\t\tif (this.closed) return;\n\t\t\tthis.refreshStatusMessage = \"\";\n\t\t\tthis.errorMessage = timedOut\n\t\t\t\t? \"Model refresh timed out; showing cached models.\"\n\t\t\t\t: `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`;\n\t\t\tthis.updateList();\n\t\t\tthis.tui.requestRender();\n\t\t} finally {\n\t\t\tif (this.refreshTimeout) clearTimeout(this.refreshTimeout);\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tif (this.refreshTimeout) clearTimeout(this.refreshTimeout);\n\t\tthis.refreshAbortController.abort();\n\t}\n\n\tprivate sortModels(models: ModelItem[]): ModelItem[] {\n\t\tconst sorted = [...models];\n\t\t// Sort: current model first, default model second, then by provider.\n\t\tsorted.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\tconst aIsDefault = this.isDefaultModel(a.model);\n\t\t\tconst bIsDefault = this.isDefaultModel(b.model);\n\t\t\tif (aIsDefault && !bIsDefault) return -1;\n\t\t\tif (!aIsDefault && bIsDefault) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\t\treturn sorted;\n\t}\n\n\tprivate rebuildRows(): void {\n\t\tif (this.step.kind === \"providers\") {\n\t\t\tconst byProvider = new Map<string, ModelItem[]>();\n\t\t\tfor (const item of this.activeModels) {\n\t\t\t\tconst existing = byProvider.get(item.provider);\n\t\t\t\tif (existing) existing.push(item);\n\t\t\t\telse byProvider.set(item.provider, [item]);\n\t\t\t}\n\t\t\tconst providerRows: Row[] = [...byProvider].map(([provider, items]) => ({\n\t\t\t\tkind: \"provider\",\n\t\t\t\tprovider,\n\t\t\t\tcount: items.length,\n\t\t\t\thasCurrent: items.some((item) => modelsAreEqual(this.currentModel, item.model)),\n\t\t\t}));\n\t\t\tproviderRows.sort((a, b) => {\n\t\t\t\tif (a.kind !== \"provider\" || b.kind !== \"provider\") return 0;\n\t\t\t\tif (a.hasCurrent !== b.hasCurrent) return a.hasCurrent ? -1 : 1;\n\t\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t\t});\n\t\t\tthis.rows = providerRows;\n\t\t\treturn;\n\t\t}\n\t\tconst provider = this.step.provider;\n\t\tconst models = provider === null ? this.activeModels : this.activeModels.filter((m) => m.provider === provider);\n\t\tthis.rows = models.map((item) => ({ kind: \"model\", item }));\n\t}\n\n\tprivate defaultSelectedIndex(): number {\n\t\tconst index = this.rows.findIndex((row) =>\n\t\t\trow.kind === \"provider\" ? row.hasCurrent : modelsAreEqual(this.currentModel, row.item.model),\n\t\t);\n\t\treturn index >= 0 ? index : 0;\n\t}\n\n\tprivate setStep(step: Step): void {\n\t\tthis.step = step;\n\t\tthis.navigatedIntoList = false;\n\t\tthis.searchInput.setValue(\"\");\n\t\tthis.rebuildRows();\n\t\tthis.selectedIndex = this.defaultSelectedIndex();\n\t\tthis.renderHeader();\n\t\tthis.filterRows(\"\");\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate isDrilledIn(): boolean {\n\t\treturn this.step.kind === \"models\" && this.step.provider !== null;\n\t}\n\n\t/** Escape returns to the provider list whenever there is a provider list worth returning to. */\n\tprivate canGoBack(): boolean {\n\t\treturn this.step.kind === \"models\" && this.activeProviders().length > 1;\n\t}\n\n\tprivate renderHeader(): void {\n\t\tthis.headerContainer.clear();\n\n\t\tif (this.step.kind === \"providers\") {\n\t\t\tthis.headerContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Select a provider\")), 0, 0));\n\t\t} else {\n\t\t\tconst label = this.step.provider ?? \"All providers\";\n\t\t\tconst crumb = `${theme.bold(theme.fg(\"accent\", label))}${theme.fg(\"muted\", \" › select a model\")}`;\n\t\t\tthis.headerContainer.addChild(new Text(crumb, 0, 0));\n\t\t}\n\n\t\tif (this.scopedModelItems.length > 0) {\n\t\t\tthis.headerContainer.addChild(new Text(this.getScopeText(), 0, 0));\n\t\t} else if (!this.isDrilledIn()) {\n\t\t\tconst hintText = \"Only showing models from configured providers. Use /login to add providers.\";\n\t\t\tthis.headerContainer.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\n\t\t}\n\n\t\tthis.renderFooter();\n\t}\n\n\tprivate renderFooter(): void {\n\t\tthis.footerContainer.clear();\n\t\tthis.footerContainer.addChild(new Text(this.getHintText(), 0, 0));\n\t}\n\n\tprivate getScopeText(): string {\n\t\tconst allText = this.scope === \"all\" ? theme.fg(\"accent\", \"all\") : theme.fg(\"muted\", \"all\");\n\t\tconst scopedText = this.scope === \"scoped\" ? theme.fg(\"accent\", \"scoped\") : theme.fg(\"muted\", \"scoped\");\n\t\treturn `${theme.fg(\"muted\", \"Scope: \")}${allText}${theme.fg(\"muted\", \" | \")}${scopedText}`;\n\t}\n\n\tprivate getHintText(): string {\n\t\tconst hints: Array<readonly [HintKey, string]> = [\n\t\t\t[[\"tui.select.up\", \"tui.select.down\"], \"move\"],\n\t\t\t[\"tui.select.confirm\", \"select\"],\n\t\t];\n\t\tif (this.hasAdjustableEffort()) hints.push([[\"tui.editor.cursorLeft\", \"tui.editor.cursorRight\"], \"effort\"]);\n\t\tif (this.scopedModelItems.length > 0) hints.push([\"tui.input.tab\", \"scope\"]);\n\t\tif (this.onSelectAsDefaultCallback) hints.push([{ literal: \"ctrl+s\" }, \"set as default\"]);\n\t\tif (this.canGoBack()) hints.push([{ literal: \"escape\" }, \"providers\"], [{ literal: \"ctrl+c\" }, \"close\"]);\n\t\telse hints.push([\"tui.select.cancel\", \"close\"]);\n\t\treturn hintRow(hints);\n\t}\n\n\t/** Levels worth dialling. A model with nothing but \"off\" has no cluster to show. */\n\tprivate getSelectableLevels(item: ModelItem): ModelThinkingLevel[] {\n\t\tconst levels = getSupportedThinkingLevels(item.model);\n\t\tif (levels.length === 1 && levels[0] === \"off\") return [];\n\t\treturn levels;\n\t}\n\n\tprivate getEffort(item: ModelItem): ModelThinkingLevel | undefined {\n\t\tconst levels = this.getSelectableLevels(item);\n\t\tif (levels.length === 0) return undefined;\n\t\tconst key = `${item.provider}/${item.id}`;\n\t\tconst stored = this.effortLevels.get(key);\n\t\tif (stored !== undefined && levels.includes(stored)) return stored;\n\t\tconst requested = this.initialThinkingLevel ?? \"off\";\n\t\tconst clamped = levels.includes(requested) ? requested : clampThinkingLevel(item.model, requested);\n\t\tconst resolved = levels.includes(clamped) ? clamped : levels[0]!;\n\t\tthis.effortLevels.set(key, resolved);\n\t\treturn resolved;\n\t}\n\n\tprivate adjustEffort(item: ModelItem, direction: number): boolean {\n\t\tconst levels = this.getSelectableLevels(item);\n\t\tif (levels.length === 0) return false;\n\t\tconst key = `${item.provider}/${item.id}`;\n\t\tconst current = this.getEffort(item) ?? levels[0]!;\n\t\tconst next = levels[(levels.indexOf(current) + direction + levels.length) % levels.length]!;\n\t\tthis.effortLevels.set(key, next);\n\t\tthis.editedEffortModels.add(key);\n\t\treturn true;\n\t}\n\n\t/** The effort to hand back on select, or undefined when the user left it alone. */\n\tprivate getChosenEffort(item: ModelItem): ModelThinkingLevel | undefined {\n\t\treturn this.editedEffortModels.has(`${item.provider}/${item.id}`) ? this.getEffort(item) : undefined;\n\t}\n\n\tprivate hasAdjustableEffort(): boolean {\n\t\treturn this.filteredRows.some((row) => row.kind === \"model\" && this.getSelectableLevels(row.item).length > 0);\n\t}\n\n\t/**\n\t * Context that trails a model row, right-aligned, in reading order.\n\t *\n\t * Each segment carries what it costs to lose, because a narrow row drops by\n\t * that and not by position. `current` outranks the id: the row already shows\n\t * the model's name, so the id is largely recoverable from it, while nothing\n\t * else on screen says which model the session is on. The provider badge goes\n\t * first, being the least you lose.\n\t */\n\tprivate getTrailingSegments(item: ModelItem): TrailingSegment[] {\n\t\tconst segments: TrailingSegment[] = [];\n\t\tif (this.step.kind === \"models\" && this.step.provider === null) {\n\t\t\tsegments.push({ text: theme.fg(\"muted\", item.provider), priority: 0 });\n\t\t}\n\t\tsegments.push({ text: theme.fg(\"muted\", item.id), priority: 2 });\n\t\tif (this.isDefaultModel(item.model)) segments.push({ text: theme.fg(\"muted\", \"default\"), priority: 1 });\n\t\tif (modelsAreEqual(this.currentModel, item.model)) {\n\t\t\tsegments.push({ text: theme.fg(\"success\", \"current\"), priority: 3 });\n\t\t}\n\t\treturn segments;\n\t}\n\n\tprivate isDefaultModel(model: Model<any>): boolean {\n\t\treturn this.defaultModel?.provider === model.provider && this.defaultModel.id === model.id;\n\t}\n\n\tprivate isDefaultSearch(query: string): boolean {\n\t\tconst normalized = query.trim().toLowerCase();\n\t\treturn normalized.length > 0 && \"default\".startsWith(normalized);\n\t}\n\n\tprivate setScope(scope: ModelScope): void {\n\t\tif (this.scope === scope) return;\n\t\tthis.scope = scope;\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tthis.rebuildRows();\n\t\tthis.selectedIndex = this.defaultSelectedIndex();\n\t\tthis.renderHeader();\n\t\tthis.filterRows(this.searchInput.getValue());\n\t}\n\n\tprivate rowSearchText(row: Row): string {\n\t\tif (row.kind === \"provider\") return row.provider;\n\t\tconst defaultText = this.isDefaultModel(row.item.model) ? \" default\" : \"\";\n\t\treturn `${getModelSelectorSearchText({ id: row.item.id, provider: row.item.provider, name: row.item.model.name })}${defaultText}`;\n\t}\n\n\tprivate isDefaultRow(row: Row): boolean {\n\t\treturn row.kind === \"model\" && this.isDefaultModel(row.item.model);\n\t}\n\n\tprivate filterRows(query: string): void {\n\t\tif (!query) {\n\t\t\tthis.filteredRows = this.rows;\n\t\t} else {\n\t\t\tconst filtered = fuzzyFilter(this.rows, query, (row) => this.rowSearchText(row));\n\t\t\tif (this.isDefaultSearch(query)) {\n\t\t\t\t// Typing \"default\" (or a prefix of it) surfaces the default model even\n\t\t\t\t// when its name doesn't otherwise fuzzy-match the query.\n\t\t\t\tconst defaultRows = this.rows.filter((row) => this.isDefaultRow(row));\n\t\t\t\tthis.filteredRows = [...defaultRows, ...filtered.filter((row) => !this.isDefaultRow(row))];\n\t\t\t} else {\n\t\t\t\tthis.filteredRows = filtered;\n\t\t\t}\n\t\t}\n\t\t// When filtering by a query, move the selector to the top row so the best\n\t\t// match is highlighted. When the query is cleared, keep the current position\n\t\t// clamped to the (restored) list length.\n\t\tthis.selectedIndex = query ? 0 : Math.min(this.selectedIndex, Math.max(0, this.filteredRows.length - 1));\n\t\t// The effort hint depends on what survived the filter, so the footer is\n\t\t// rebuilt here rather than with the header, which runs before filtering.\n\t\tthis.renderFooter();\n\t\tthis.updateList();\n\t}\n\n\tprivate renderProviderRow(row: Extract<Row, { kind: \"provider\" }>, isSelected: boolean): string {\n\t\tconst prefix = isSelected ? \"→ \" : \" \";\n\t\tconst count = theme.fg(\"muted\", ` (${row.count} model${row.count === 1 ? \"\" : \"s\"})`);\n\t\tconst checkmark = row.hasCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\tconst composed = `${prefix}${row.provider}${count}${checkmark}`;\n\t\treturn isSelected ? paintSelectedRow(composed) : composed;\n\t}\n\n\t/**\n\t * The visible page of rows, rendered together because the effort cluster is\n\t * placed once for the whole page rather than per row.\n\t */\n\tprivate createRowsComponent(startIndex: number, endIndex: number): Component {\n\t\tconst rows = this.filteredRows.slice(startIndex, endIndex);\n\t\treturn {\n\t\t\trender: (width: number): string[] => {\n\t\t\t\tconst layout = this.pageEffortLayout(rows, width);\n\t\t\t\treturn rows.map((row, offset) => {\n\t\t\t\t\tconst isSelected = startIndex + offset === this.selectedIndex;\n\t\t\t\t\tif (row.kind === \"provider\") return this.renderProviderRow(row, isSelected);\n\t\t\t\t\tconst composed = composeModelRow({\n\t\t\t\t\t\tname: row.item.model.name,\n\t\t\t\t\t\tlevels: this.getSelectableLevels(row.item),\n\t\t\t\t\t\teffort: this.getEffort(row.item),\n\t\t\t\t\t\ttrailingSegments: this.getTrailingSegments(row.item),\n\t\t\t\t\t\tselected: isSelected,\n\t\t\t\t\t\tlayout,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t});\n\t\t\t\t\treturn isSelected ? paintSelectedRowToWidth(composed, width) : composed;\n\t\t\t\t});\n\t\t\t},\n\t\t\tinvalidate: () => {},\n\t\t};\n\t}\n\n\tprivate pageEffortLayout(rows: ReadonlyArray<Row>, width: number): EffortLayout {\n\t\tconst layoutRows: EffortLayoutRow[] = [];\n\t\tfor (const row of rows) {\n\t\t\tif (row.kind !== \"model\") continue;\n\t\t\tlayoutRows.push({\n\t\t\t\tname: row.item.model.name,\n\t\t\t\tlevels: this.getSelectableLevels(row.item),\n\t\t\t\ttrailingSegments: this.getTrailingSegments(row.item),\n\t\t\t});\n\t\t}\n\t\treturn layoutRows.length > 0 ? getEffortLayout(layoutRows, width) : NO_EFFORT_CLUSTER;\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(MAX_VISIBLE_ROWS / 2), this.filteredRows.length - MAX_VISIBLE_ROWS),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + MAX_VISIBLE_ROWS, this.filteredRows.length);\n\n\t\tif (endIndex > startIndex) {\n\t\t\tthis.listContainer.addChild(this.createRowsComponent(startIndex, endIndex));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredRows.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredRows.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.filteredRows.length === 0) {\n\t\t\tconst empty = this.step.kind === \"providers\" ? \" No matching providers\" : \" No matching models\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", empty), 0, 0));\n\t\t} else {\n\t\t\tconst selected = this.filteredRows[this.selectedIndex];\n\t\t\tif (selected?.kind === \"model\") {\n\t\t\t\tconst cost = selected.item.model.cost;\n\t\t\t\tthis.listContainer.addChild({\n\t\t\t\t\trender: (width: number) => renderPricePanel(cost, width),\n\t\t\t\t\tinvalidate: () => {},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (this.refreshStatusMessage) {\n\t\t\tthis.listContainer.addChild(new Spacer(1));\n\t\t\tthis.listContainer.addChild(\n\t\t\t\tnew Text(theme.fg(this.refreshStatusSuccess ? \"success\" : \"muted\", ` ${this.refreshStatusMessage}`), 0, 0),\n\t\t\t);\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tif (this.scopedModelItems.length > 0) {\n\t\t\t\tthis.setScope(this.scope === \"all\" ? \"scoped\" : \"all\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Left/right dial the highlighted model's effort, but only once they are\n\t\t// free to: while a query is being typed they still drive the search cursor.\n\t\tif (\n\t\t\t(this.searchInput.getValue() === \"\" || this.navigatedIntoList) &&\n\t\t\t(kb.matches(keyData, \"tui.editor.cursorLeft\") || kb.matches(keyData, \"tui.editor.cursorRight\"))\n\t\t) {\n\t\t\tconst row = this.filteredRows[this.selectedIndex];\n\t\t\tconst direction = kb.matches(keyData, \"tui.editor.cursorLeft\") ? -1 : 1;\n\t\t\tif (row?.kind === \"model\" && this.adjustEffort(row.item, direction)) {\n\t\t\t\tthis.updateList();\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filteredRows.length === 0) return;\n\t\t\tthis.navigatedIntoList = true;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredRows.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 (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filteredRows.length === 0) return;\n\t\t\tthis.navigatedIntoList = true;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredRows.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tthis.confirmSelection();\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\t// Escape steps back through the two levels; Ctrl+C always closes.\n\t\t\tif (matchesKey(keyData, Key.escape) && this.canGoBack()) {\n\t\t\t\tthis.setStep({ kind: \"providers\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.dispose();\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Ctrl+S — select and save as default\n\t\telse if (matchesKey(keyData, \"ctrl+s\") && this.onSelectAsDefaultCallback) {\n\t\t\tconst row = this.filteredRows[this.selectedIndex];\n\t\t\tif (row?.kind === \"model\") {\n\t\t\t\tthis.dispose();\n\t\t\t\tthis.onSelectAsDefaultCallback(row.item.model, this.getChosenEffort(row.item));\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tconst previousQuery = this.searchInput.getValue();\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tif (previousQuery !== this.searchInput.getValue()) {\n\t\t\t\tthis.navigatedIntoList = false;\n\t\t\t}\n\t\t\tthis.filterRows(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate confirmSelection(): void {\n\t\tconst row = this.filteredRows[this.selectedIndex];\n\t\tif (!row) return;\n\t\tif (row.kind === \"provider\") {\n\t\t\tthis.setStep({ kind: \"models\", provider: row.provider });\n\t\t\treturn;\n\t\t}\n\t\tthis.dispose();\n\t\tthis.onSelectCallback(row.item.model, this.getChosenEffort(row.item));\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}