indusagi-coding-agent 0.1.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 (240) hide show
  1. package/CHANGELOG.md +2249 -0
  2. package/README.md +546 -0
  3. package/dist/cli/args.js +282 -0
  4. package/dist/cli/config-selector.js +30 -0
  5. package/dist/cli/file-processor.js +78 -0
  6. package/dist/cli/list-models.js +91 -0
  7. package/dist/cli/session-picker.js +31 -0
  8. package/dist/cli.js +10 -0
  9. package/dist/config.js +158 -0
  10. package/dist/core/agent-session.js +2097 -0
  11. package/dist/core/auth-storage.js +278 -0
  12. package/dist/core/bash-executor.js +211 -0
  13. package/dist/core/compaction/branch-summarization.js +241 -0
  14. package/dist/core/compaction/compaction.js +606 -0
  15. package/dist/core/compaction/index.js +6 -0
  16. package/dist/core/compaction/utils.js +137 -0
  17. package/dist/core/diagnostics.js +1 -0
  18. package/dist/core/event-bus.js +24 -0
  19. package/dist/core/exec.js +70 -0
  20. package/dist/core/export-html/ansi-to-html.js +248 -0
  21. package/dist/core/export-html/index.js +221 -0
  22. package/dist/core/export-html/template.css +905 -0
  23. package/dist/core/export-html/template.html +54 -0
  24. package/dist/core/export-html/template.js +1549 -0
  25. package/dist/core/export-html/tool-renderer.js +56 -0
  26. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  27. package/dist/core/export-html/vendor/marked.min.js +6 -0
  28. package/dist/core/extensions/index.js +8 -0
  29. package/dist/core/extensions/loader.js +395 -0
  30. package/dist/core/extensions/runner.js +499 -0
  31. package/dist/core/extensions/types.js +31 -0
  32. package/dist/core/extensions/wrapper.js +101 -0
  33. package/dist/core/footer-data-provider.js +133 -0
  34. package/dist/core/index.js +8 -0
  35. package/dist/core/keybindings.js +140 -0
  36. package/dist/core/messages.js +122 -0
  37. package/dist/core/model-registry.js +454 -0
  38. package/dist/core/model-resolver.js +309 -0
  39. package/dist/core/package-manager.js +1142 -0
  40. package/dist/core/prompt-templates.js +250 -0
  41. package/dist/core/resource-loader.js +569 -0
  42. package/dist/core/sdk.js +225 -0
  43. package/dist/core/session-manager.js +1078 -0
  44. package/dist/core/settings-manager.js +430 -0
  45. package/dist/core/skills.js +339 -0
  46. package/dist/core/system-prompt.js +136 -0
  47. package/dist/core/timings.js +24 -0
  48. package/dist/core/tools/bash.js +226 -0
  49. package/dist/core/tools/edit-diff.js +242 -0
  50. package/dist/core/tools/edit.js +145 -0
  51. package/dist/core/tools/find.js +205 -0
  52. package/dist/core/tools/grep.js +238 -0
  53. package/dist/core/tools/index.js +60 -0
  54. package/dist/core/tools/ls.js +117 -0
  55. package/dist/core/tools/path-utils.js +52 -0
  56. package/dist/core/tools/read.js +165 -0
  57. package/dist/core/tools/truncate.js +204 -0
  58. package/dist/core/tools/write.js +77 -0
  59. package/dist/index.js +41 -0
  60. package/dist/main.js +565 -0
  61. package/dist/migrations.js +260 -0
  62. package/dist/modes/index.js +7 -0
  63. package/dist/modes/interactive/components/armin.js +328 -0
  64. package/dist/modes/interactive/components/assistant-message.js +86 -0
  65. package/dist/modes/interactive/components/bash-execution.js +155 -0
  66. package/dist/modes/interactive/components/bordered-loader.js +47 -0
  67. package/dist/modes/interactive/components/branch-summary-message.js +41 -0
  68. package/dist/modes/interactive/components/compaction-summary-message.js +42 -0
  69. package/dist/modes/interactive/components/config-selector.js +458 -0
  70. package/dist/modes/interactive/components/countdown-timer.js +27 -0
  71. package/dist/modes/interactive/components/custom-editor.js +61 -0
  72. package/dist/modes/interactive/components/custom-message.js +80 -0
  73. package/dist/modes/interactive/components/diff.js +132 -0
  74. package/dist/modes/interactive/components/dynamic-border.js +19 -0
  75. package/dist/modes/interactive/components/extension-editor.js +96 -0
  76. package/dist/modes/interactive/components/extension-input.js +54 -0
  77. package/dist/modes/interactive/components/extension-selector.js +70 -0
  78. package/dist/modes/interactive/components/footer.js +213 -0
  79. package/dist/modes/interactive/components/index.js +31 -0
  80. package/dist/modes/interactive/components/keybinding-hints.js +60 -0
  81. package/dist/modes/interactive/components/login-dialog.js +138 -0
  82. package/dist/modes/interactive/components/model-selector.js +253 -0
  83. package/dist/modes/interactive/components/oauth-selector.js +91 -0
  84. package/dist/modes/interactive/components/scoped-models-selector.js +262 -0
  85. package/dist/modes/interactive/components/session-selector-search.js +145 -0
  86. package/dist/modes/interactive/components/session-selector.js +698 -0
  87. package/dist/modes/interactive/components/settings-selector.js +250 -0
  88. package/dist/modes/interactive/components/show-images-selector.js +33 -0
  89. package/dist/modes/interactive/components/skill-invocation-message.js +44 -0
  90. package/dist/modes/interactive/components/theme-selector.js +43 -0
  91. package/dist/modes/interactive/components/thinking-selector.js +45 -0
  92. package/dist/modes/interactive/components/tool-execution.js +608 -0
  93. package/dist/modes/interactive/components/tree-selector.js +892 -0
  94. package/dist/modes/interactive/components/user-message-selector.js +109 -0
  95. package/dist/modes/interactive/components/user-message.js +15 -0
  96. package/dist/modes/interactive/components/visual-truncate.js +32 -0
  97. package/dist/modes/interactive/interactive-mode.js +3576 -0
  98. package/dist/modes/interactive/theme/dark.json +85 -0
  99. package/dist/modes/interactive/theme/light.json +84 -0
  100. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  101. package/dist/modes/interactive/theme/theme.js +938 -0
  102. package/dist/modes/print-mode.js +96 -0
  103. package/dist/modes/rpc/rpc-client.js +390 -0
  104. package/dist/modes/rpc/rpc-mode.js +448 -0
  105. package/dist/modes/rpc/rpc-types.js +7 -0
  106. package/dist/utils/changelog.js +86 -0
  107. package/dist/utils/clipboard-image.js +116 -0
  108. package/dist/utils/clipboard.js +58 -0
  109. package/dist/utils/frontmatter.js +25 -0
  110. package/dist/utils/git.js +5 -0
  111. package/dist/utils/image-convert.js +34 -0
  112. package/dist/utils/image-resize.js +180 -0
  113. package/dist/utils/mime.js +25 -0
  114. package/dist/utils/photon.js +120 -0
  115. package/dist/utils/shell.js +164 -0
  116. package/dist/utils/sleep.js +16 -0
  117. package/dist/utils/tools-manager.js +186 -0
  118. package/docs/compaction.md +390 -0
  119. package/docs/custom-provider.md +538 -0
  120. package/docs/development.md +69 -0
  121. package/docs/extensions.md +1733 -0
  122. package/docs/images/doom-extension.png +0 -0
  123. package/docs/images/interactive-mode.png +0 -0
  124. package/docs/images/tree-view.png +0 -0
  125. package/docs/json.md +79 -0
  126. package/docs/keybindings.md +162 -0
  127. package/docs/models.md +193 -0
  128. package/docs/packages.md +163 -0
  129. package/docs/prompt-templates.md +67 -0
  130. package/docs/providers.md +147 -0
  131. package/docs/rpc.md +1048 -0
  132. package/docs/sdk.md +957 -0
  133. package/docs/session.md +412 -0
  134. package/docs/settings.md +216 -0
  135. package/docs/shell-aliases.md +13 -0
  136. package/docs/skills.md +226 -0
  137. package/docs/terminal-setup.md +65 -0
  138. package/docs/themes.md +295 -0
  139. package/docs/tree.md +219 -0
  140. package/docs/tui.md +887 -0
  141. package/docs/windows.md +17 -0
  142. package/examples/README.md +25 -0
  143. package/examples/extensions/README.md +192 -0
  144. package/examples/extensions/antigravity-image-gen.ts +414 -0
  145. package/examples/extensions/auto-commit-on-exit.ts +49 -0
  146. package/examples/extensions/bookmark.ts +50 -0
  147. package/examples/extensions/claude-rules.ts +86 -0
  148. package/examples/extensions/confirm-destructive.ts +59 -0
  149. package/examples/extensions/custom-compaction.ts +115 -0
  150. package/examples/extensions/custom-footer.ts +65 -0
  151. package/examples/extensions/custom-header.ts +73 -0
  152. package/examples/extensions/custom-provider-anthropic/index.ts +605 -0
  153. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  154. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  155. package/examples/extensions/custom-provider-gitlab-duo/index.ts +350 -0
  156. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  157. package/examples/extensions/custom-provider-gitlab-duo/test.ts +83 -0
  158. package/examples/extensions/dirty-repo-guard.ts +56 -0
  159. package/examples/extensions/doom-overlay/README.md +46 -0
  160. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  161. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  162. package/examples/extensions/doom-overlay/doom/build.sh +152 -0
  163. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  164. package/examples/extensions/doom-overlay/doom-component.ts +133 -0
  165. package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  166. package/examples/extensions/doom-overlay/doom-keys.ts +105 -0
  167. package/examples/extensions/doom-overlay/index.ts +74 -0
  168. package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  169. package/examples/extensions/event-bus.ts +43 -0
  170. package/examples/extensions/file-trigger.ts +41 -0
  171. package/examples/extensions/git-checkpoint.ts +53 -0
  172. package/examples/extensions/handoff.ts +151 -0
  173. package/examples/extensions/hello.ts +25 -0
  174. package/examples/extensions/inline-bash.ts +94 -0
  175. package/examples/extensions/input-transform.ts +43 -0
  176. package/examples/extensions/interactive-shell.ts +196 -0
  177. package/examples/extensions/mac-system-theme.ts +47 -0
  178. package/examples/extensions/message-renderer.ts +60 -0
  179. package/examples/extensions/modal-editor.ts +86 -0
  180. package/examples/extensions/model-status.ts +31 -0
  181. package/examples/extensions/notify.ts +25 -0
  182. package/examples/extensions/overlay-qa-tests.ts +882 -0
  183. package/examples/extensions/overlay-test.ts +151 -0
  184. package/examples/extensions/permission-gate.ts +34 -0
  185. package/examples/extensions/pirate.ts +47 -0
  186. package/examples/extensions/plan-mode/README.md +65 -0
  187. package/examples/extensions/plan-mode/index.ts +341 -0
  188. package/examples/extensions/plan-mode/utils.ts +168 -0
  189. package/examples/extensions/preset.ts +399 -0
  190. package/examples/extensions/protected-paths.ts +30 -0
  191. package/examples/extensions/qna.ts +120 -0
  192. package/examples/extensions/question.ts +265 -0
  193. package/examples/extensions/questionnaire.ts +428 -0
  194. package/examples/extensions/rainbow-editor.ts +88 -0
  195. package/examples/extensions/sandbox/index.ts +318 -0
  196. package/examples/extensions/sandbox/package-lock.json +92 -0
  197. package/examples/extensions/sandbox/package.json +19 -0
  198. package/examples/extensions/send-user-message.ts +97 -0
  199. package/examples/extensions/session-name.ts +27 -0
  200. package/examples/extensions/shutdown-command.ts +63 -0
  201. package/examples/extensions/snake.ts +344 -0
  202. package/examples/extensions/space-invaders.ts +561 -0
  203. package/examples/extensions/ssh.ts +220 -0
  204. package/examples/extensions/status-line.ts +40 -0
  205. package/examples/extensions/subagent/README.md +172 -0
  206. package/examples/extensions/subagent/agents/planner.md +37 -0
  207. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  208. package/examples/extensions/subagent/agents/scout.md +50 -0
  209. package/examples/extensions/subagent/agents/worker.md +24 -0
  210. package/examples/extensions/subagent/agents.ts +127 -0
  211. package/examples/extensions/subagent/index.ts +964 -0
  212. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  213. package/examples/extensions/subagent/prompts/implement.md +10 -0
  214. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  215. package/examples/extensions/summarize.ts +196 -0
  216. package/examples/extensions/timed-confirm.ts +70 -0
  217. package/examples/extensions/todo.ts +300 -0
  218. package/examples/extensions/tool-override.ts +144 -0
  219. package/examples/extensions/tools.ts +147 -0
  220. package/examples/extensions/trigger-compact.ts +40 -0
  221. package/examples/extensions/truncated-tool.ts +193 -0
  222. package/examples/extensions/widget-placement.ts +17 -0
  223. package/examples/extensions/with-deps/index.ts +36 -0
  224. package/examples/extensions/with-deps/package-lock.json +31 -0
  225. package/examples/extensions/with-deps/package.json +22 -0
  226. package/examples/sdk/01-minimal.ts +22 -0
  227. package/examples/sdk/02-custom-model.ts +50 -0
  228. package/examples/sdk/03-custom-prompt.ts +55 -0
  229. package/examples/sdk/04-skills.ts +46 -0
  230. package/examples/sdk/05-tools.ts +56 -0
  231. package/examples/sdk/06-extensions.ts +88 -0
  232. package/examples/sdk/07-context-files.ts +40 -0
  233. package/examples/sdk/08-prompt-templates.ts +47 -0
  234. package/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  235. package/examples/sdk/10-settings.ts +38 -0
  236. package/examples/sdk/11-sessions.ts +48 -0
  237. package/examples/sdk/12-full-control.ts +82 -0
  238. package/examples/sdk/13-codex-oauth.ts +37 -0
  239. package/examples/sdk/README.md +144 -0
  240. package/package.json +85 -0
@@ -0,0 +1,138 @@
1
+ import { getOAuthProviders } from "indusagi/ai";
2
+ import { Container, getEditorKeybindings, Input, Spacer, Text } from "indusagi/tui";
3
+ import { exec } from "child_process";
4
+ import { theme } from "../theme/theme.js";
5
+ import { DynamicBorder } from "./dynamic-border.js";
6
+ import { keyHint } from "./keybinding-hints.js";
7
+ /**
8
+ * Login dialog component - replaces editor during OAuth login flow
9
+ */
10
+ export class LoginDialogComponent extends Container {
11
+ get focused() {
12
+ return this._focused;
13
+ }
14
+ set focused(value) {
15
+ this._focused = value;
16
+ this.input.focused = value;
17
+ }
18
+ constructor(tui, providerId, onComplete) {
19
+ super();
20
+ this.onComplete = onComplete;
21
+ this.abortController = new AbortController();
22
+ // Focusable implementation - propagate to input for IME cursor positioning
23
+ this._focused = false;
24
+ this.tui = tui;
25
+ const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
26
+ const providerName = providerInfo?.name || providerId;
27
+ // Top border
28
+ this.addChild(new DynamicBorder());
29
+ // Title
30
+ this.addChild(new Text(theme.fg("warning", `Login to ${providerName}`), 1, 0));
31
+ // Dynamic content area
32
+ this.contentContainer = new Container();
33
+ this.addChild(this.contentContainer);
34
+ // Input (always present, used when needed)
35
+ this.input = new Input();
36
+ this.input.onSubmit = () => {
37
+ if (this.inputResolver) {
38
+ this.inputResolver(this.input.getValue());
39
+ this.inputResolver = undefined;
40
+ this.inputRejecter = undefined;
41
+ }
42
+ };
43
+ this.input.onEscape = () => {
44
+ this.cancel();
45
+ };
46
+ // Bottom border
47
+ this.addChild(new DynamicBorder());
48
+ }
49
+ get signal() {
50
+ return this.abortController.signal;
51
+ }
52
+ cancel() {
53
+ this.abortController.abort();
54
+ if (this.inputRejecter) {
55
+ this.inputRejecter(new Error("Login cancelled"));
56
+ this.inputResolver = undefined;
57
+ this.inputRejecter = undefined;
58
+ }
59
+ this.onComplete(false, "Login cancelled");
60
+ }
61
+ /**
62
+ * Called by onAuth callback - show URL and optional instructions
63
+ */
64
+ showAuth(url, instructions) {
65
+ this.contentContainer.clear();
66
+ this.contentContainer.addChild(new Spacer(1));
67
+ this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
68
+ const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
69
+ const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
70
+ this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
71
+ if (instructions) {
72
+ this.contentContainer.addChild(new Spacer(1));
73
+ this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
74
+ }
75
+ // Try to open browser
76
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
77
+ exec(`${openCmd} "${url}"`);
78
+ this.tui.requestRender();
79
+ }
80
+ /**
81
+ * Show input for manual code/URL entry (for callback server providers)
82
+ */
83
+ showManualInput(prompt) {
84
+ this.contentContainer.addChild(new Spacer(1));
85
+ this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
86
+ this.contentContainer.addChild(this.input);
87
+ this.contentContainer.addChild(new Text(`(${keyHint("selectCancel", "to cancel")})`, 1, 0));
88
+ this.tui.requestRender();
89
+ return new Promise((resolve, reject) => {
90
+ this.inputResolver = resolve;
91
+ this.inputRejecter = reject;
92
+ });
93
+ }
94
+ /**
95
+ * Called by onPrompt callback - show prompt and wait for input
96
+ * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)
97
+ */
98
+ showPrompt(message, placeholder) {
99
+ this.contentContainer.addChild(new Spacer(1));
100
+ this.contentContainer.addChild(new Text(theme.fg("text", message), 1, 0));
101
+ if (placeholder) {
102
+ this.contentContainer.addChild(new Text(theme.fg("dim", `e.g., ${placeholder}`), 1, 0));
103
+ }
104
+ this.contentContainer.addChild(this.input);
105
+ this.contentContainer.addChild(new Text(`(${keyHint("selectCancel", "to cancel,")} ${keyHint("selectConfirm", "to submit")})`, 1, 0));
106
+ this.input.setValue("");
107
+ this.tui.requestRender();
108
+ return new Promise((resolve, reject) => {
109
+ this.inputResolver = resolve;
110
+ this.inputRejecter = reject;
111
+ });
112
+ }
113
+ /**
114
+ * Show waiting message (for polling flows like GitHub Copilot)
115
+ */
116
+ showWaiting(message) {
117
+ this.contentContainer.addChild(new Spacer(1));
118
+ this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
119
+ this.contentContainer.addChild(new Text(`(${keyHint("selectCancel", "to cancel")})`, 1, 0));
120
+ this.tui.requestRender();
121
+ }
122
+ /**
123
+ * Called by onProgress callback
124
+ */
125
+ showProgress(message) {
126
+ this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
127
+ this.tui.requestRender();
128
+ }
129
+ handleInput(data) {
130
+ const kb = getEditorKeybindings();
131
+ if (kb.matches(data, "selectCancel")) {
132
+ this.cancel();
133
+ return;
134
+ }
135
+ // Pass to input
136
+ this.input.handleInput(data);
137
+ }
138
+ }
@@ -0,0 +1,253 @@
1
+ import { modelsAreEqual } from "indusagi/ai";
2
+ import { Container, fuzzyFilter, getEditorKeybindings, Input, Spacer, Text, } from "indusagi/tui";
3
+ import { theme } from "../theme/theme.js";
4
+ import { DynamicBorder } from "./dynamic-border.js";
5
+ import { keyHint } from "./keybinding-hints.js";
6
+ /**
7
+ * Component that renders a model selector with search
8
+ */
9
+ export class ModelSelectorComponent extends Container {
10
+ get focused() {
11
+ return this._focused;
12
+ }
13
+ set focused(value) {
14
+ this._focused = value;
15
+ this.searchInput.focused = value;
16
+ }
17
+ constructor(tui, currentModel, settingsManager, modelRegistry, scopedModels, onSelect, onCancel, initialSearchInput) {
18
+ super();
19
+ // Focusable implementation - propagate to searchInput for IME cursor positioning
20
+ this._focused = false;
21
+ this.allModels = [];
22
+ this.scopedModelItems = [];
23
+ this.activeModels = [];
24
+ this.filteredModels = [];
25
+ this.selectedIndex = 0;
26
+ this.scope = "all";
27
+ this.tui = tui;
28
+ this.currentModel = currentModel;
29
+ this.settingsManager = settingsManager;
30
+ this.modelRegistry = modelRegistry;
31
+ this.scopedModels = scopedModels;
32
+ this.scope = scopedModels.length > 0 ? "scoped" : "all";
33
+ this.onSelectCallback = onSelect;
34
+ this.onCancelCallback = onCancel;
35
+ // Add top border
36
+ this.addChild(new DynamicBorder());
37
+ this.addChild(new Spacer(1));
38
+ // Add hint about model filtering
39
+ if (scopedModels.length > 0) {
40
+ this.scopeText = new Text(this.getScopeText(), 0, 0);
41
+ this.addChild(this.scopeText);
42
+ this.scopeHintText = new Text(this.getScopeHintText(), 0, 0);
43
+ this.addChild(this.scopeHintText);
44
+ }
45
+ else {
46
+ const hintText = "Only showing models with configured API keys (see README for details)";
47
+ this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
48
+ }
49
+ this.addChild(new Spacer(1));
50
+ // Create search input
51
+ this.searchInput = new Input();
52
+ if (initialSearchInput) {
53
+ this.searchInput.setValue(initialSearchInput);
54
+ }
55
+ this.searchInput.onSubmit = () => {
56
+ // Enter on search input selects the first filtered item
57
+ if (this.filteredModels[this.selectedIndex]) {
58
+ this.handleSelect(this.filteredModels[this.selectedIndex].model);
59
+ }
60
+ };
61
+ this.addChild(this.searchInput);
62
+ this.addChild(new Spacer(1));
63
+ // Create list container
64
+ this.listContainer = new Container();
65
+ this.addChild(this.listContainer);
66
+ this.addChild(new Spacer(1));
67
+ // Add bottom border
68
+ this.addChild(new DynamicBorder());
69
+ // Load models and do initial render
70
+ this.loadModels().then(() => {
71
+ if (initialSearchInput) {
72
+ this.filterModels(initialSearchInput);
73
+ }
74
+ else {
75
+ this.updateList();
76
+ }
77
+ // Request re-render after models are loaded
78
+ this.tui.requestRender();
79
+ });
80
+ }
81
+ async loadModels() {
82
+ let models;
83
+ // Refresh to pick up any changes to models.json
84
+ this.modelRegistry.refresh();
85
+ // Check for models.json errors
86
+ const loadError = this.modelRegistry.getError();
87
+ if (loadError) {
88
+ this.errorMessage = loadError;
89
+ }
90
+ // Load available models (built-in models still work even if models.json failed)
91
+ try {
92
+ const availableModels = await this.modelRegistry.getAvailable();
93
+ models = availableModels.map((model) => ({
94
+ provider: model.provider,
95
+ id: model.id,
96
+ model,
97
+ }));
98
+ }
99
+ catch (error) {
100
+ this.allModels = [];
101
+ this.scopedModelItems = [];
102
+ this.activeModels = [];
103
+ this.filteredModels = [];
104
+ this.errorMessage = error instanceof Error ? error.message : String(error);
105
+ return;
106
+ }
107
+ this.allModels = this.sortModels(models);
108
+ this.scopedModelItems = this.sortModels(this.scopedModels.map((scoped) => ({
109
+ provider: scoped.model.provider,
110
+ id: scoped.model.id,
111
+ model: scoped.model,
112
+ })));
113
+ this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
114
+ this.filteredModels = this.activeModels;
115
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
116
+ }
117
+ sortModels(models) {
118
+ const sorted = [...models];
119
+ // Sort: current model first, then by provider
120
+ sorted.sort((a, b) => {
121
+ const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
122
+ const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
123
+ if (aIsCurrent && !bIsCurrent)
124
+ return -1;
125
+ if (!aIsCurrent && bIsCurrent)
126
+ return 1;
127
+ return a.provider.localeCompare(b.provider);
128
+ });
129
+ return sorted;
130
+ }
131
+ getScopeText() {
132
+ const allText = this.scope === "all" ? theme.fg("accent", "all") : theme.fg("muted", "all");
133
+ const scopedText = this.scope === "scoped" ? theme.fg("accent", "scoped") : theme.fg("muted", "scoped");
134
+ return `${theme.fg("muted", "Scope: ")}${allText}${theme.fg("muted", " | ")}${scopedText}`;
135
+ }
136
+ getScopeHintText() {
137
+ return keyHint("tab", "scope") + theme.fg("muted", " (all/scoped)");
138
+ }
139
+ setScope(scope) {
140
+ if (this.scope === scope)
141
+ return;
142
+ this.scope = scope;
143
+ this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
144
+ this.selectedIndex = 0;
145
+ this.filterModels(this.searchInput.getValue());
146
+ if (this.scopeText) {
147
+ this.scopeText.setText(this.getScopeText());
148
+ }
149
+ }
150
+ filterModels(query) {
151
+ this.filteredModels = query
152
+ ? fuzzyFilter(this.activeModels, query, ({ id, provider }) => `${id} ${provider}`)
153
+ : this.activeModels;
154
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
155
+ this.updateList();
156
+ }
157
+ updateList() {
158
+ this.listContainer.clear();
159
+ const maxVisible = 10;
160
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible));
161
+ const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
162
+ // Show visible slice of filtered models
163
+ for (let i = startIndex; i < endIndex; i++) {
164
+ const item = this.filteredModels[i];
165
+ if (!item)
166
+ continue;
167
+ const isSelected = i === this.selectedIndex;
168
+ const isCurrent = modelsAreEqual(this.currentModel, item.model);
169
+ let line = "";
170
+ if (isSelected) {
171
+ const prefix = theme.fg("accent", "→ ");
172
+ const modelText = `${item.id}`;
173
+ const providerBadge = theme.fg("muted", `[${item.provider}]`);
174
+ const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
175
+ line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${checkmark}`;
176
+ }
177
+ else {
178
+ const modelText = ` ${item.id}`;
179
+ const providerBadge = theme.fg("muted", `[${item.provider}]`);
180
+ const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
181
+ line = `${modelText} ${providerBadge}${checkmark}`;
182
+ }
183
+ this.listContainer.addChild(new Text(line, 0, 0));
184
+ }
185
+ // Add scroll indicator if needed
186
+ if (startIndex > 0 || endIndex < this.filteredModels.length) {
187
+ const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
188
+ this.listContainer.addChild(new Text(scrollInfo, 0, 0));
189
+ }
190
+ // Show error message or "no results" if empty
191
+ if (this.errorMessage) {
192
+ // Show error in red
193
+ const errorLines = this.errorMessage.split("\n");
194
+ for (const line of errorLines) {
195
+ this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
196
+ }
197
+ }
198
+ else if (this.filteredModels.length === 0) {
199
+ this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
200
+ }
201
+ }
202
+ handleInput(keyData) {
203
+ const kb = getEditorKeybindings();
204
+ if (kb.matches(keyData, "tab")) {
205
+ if (this.scopedModelItems.length > 0) {
206
+ const nextScope = this.scope === "all" ? "scoped" : "all";
207
+ this.setScope(nextScope);
208
+ if (this.scopeHintText) {
209
+ this.scopeHintText.setText(this.getScopeHintText());
210
+ }
211
+ }
212
+ return;
213
+ }
214
+ // Up arrow - wrap to bottom when at top
215
+ if (kb.matches(keyData, "selectUp")) {
216
+ if (this.filteredModels.length === 0)
217
+ return;
218
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
219
+ this.updateList();
220
+ }
221
+ // Down arrow - wrap to top when at bottom
222
+ else if (kb.matches(keyData, "selectDown")) {
223
+ if (this.filteredModels.length === 0)
224
+ return;
225
+ this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
226
+ this.updateList();
227
+ }
228
+ // Enter
229
+ else if (kb.matches(keyData, "selectConfirm")) {
230
+ const selectedModel = this.filteredModels[this.selectedIndex];
231
+ if (selectedModel) {
232
+ this.handleSelect(selectedModel.model);
233
+ }
234
+ }
235
+ // Escape or Ctrl+C
236
+ else if (kb.matches(keyData, "selectCancel")) {
237
+ this.onCancelCallback();
238
+ }
239
+ // Pass everything else to search input
240
+ else {
241
+ this.searchInput.handleInput(keyData);
242
+ this.filterModels(this.searchInput.getValue());
243
+ }
244
+ }
245
+ handleSelect(model) {
246
+ // Save as new default
247
+ this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
248
+ this.onSelectCallback(model);
249
+ }
250
+ getSearchInput() {
251
+ return this.searchInput;
252
+ }
253
+ }
@@ -0,0 +1,91 @@
1
+ import { getOAuthProviders } from "indusagi/ai";
2
+ import { Container, getEditorKeybindings, Spacer, TruncatedText } from "indusagi/tui";
3
+ import { theme } from "../theme/theme.js";
4
+ import { DynamicBorder } from "./dynamic-border.js";
5
+ /**
6
+ * Component that renders an OAuth provider selector
7
+ */
8
+ export class OAuthSelectorComponent extends Container {
9
+ constructor(mode, authStorage, onSelect, onCancel) {
10
+ super();
11
+ this.allProviders = [];
12
+ this.selectedIndex = 0;
13
+ this.mode = mode;
14
+ this.authStorage = authStorage;
15
+ this.onSelectCallback = onSelect;
16
+ this.onCancelCallback = onCancel;
17
+ // Load all OAuth providers
18
+ this.loadProviders();
19
+ // Add top border
20
+ this.addChild(new DynamicBorder());
21
+ this.addChild(new Spacer(1));
22
+ // Add title
23
+ const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
24
+ this.addChild(new TruncatedText(theme.bold(title)));
25
+ this.addChild(new Spacer(1));
26
+ // Create list container
27
+ this.listContainer = new Container();
28
+ this.addChild(this.listContainer);
29
+ this.addChild(new Spacer(1));
30
+ // Add bottom border
31
+ this.addChild(new DynamicBorder());
32
+ // Initial render
33
+ this.updateList();
34
+ }
35
+ loadProviders() {
36
+ this.allProviders = getOAuthProviders();
37
+ }
38
+ updateList() {
39
+ this.listContainer.clear();
40
+ for (let i = 0; i < this.allProviders.length; i++) {
41
+ const provider = this.allProviders[i];
42
+ if (!provider)
43
+ continue;
44
+ const isSelected = i === this.selectedIndex;
45
+ // Check if user is logged in for this provider
46
+ const credentials = this.authStorage.get(provider.id);
47
+ const isLoggedIn = credentials?.type === "oauth";
48
+ const statusIndicator = isLoggedIn ? theme.fg("success", " ✓ logged in") : "";
49
+ let line = "";
50
+ if (isSelected) {
51
+ const prefix = theme.fg("accent", "→ ");
52
+ const text = theme.fg("accent", provider.name);
53
+ line = prefix + text + statusIndicator;
54
+ }
55
+ else {
56
+ const text = ` ${provider.name}`;
57
+ line = text + statusIndicator;
58
+ }
59
+ this.listContainer.addChild(new TruncatedText(line, 0, 0));
60
+ }
61
+ // Show "no providers" if empty
62
+ if (this.allProviders.length === 0) {
63
+ const message = this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
64
+ this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
65
+ }
66
+ }
67
+ handleInput(keyData) {
68
+ const kb = getEditorKeybindings();
69
+ // Up arrow
70
+ if (kb.matches(keyData, "selectUp")) {
71
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
72
+ this.updateList();
73
+ }
74
+ // Down arrow
75
+ else if (kb.matches(keyData, "selectDown")) {
76
+ this.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);
77
+ this.updateList();
78
+ }
79
+ // Enter
80
+ else if (kb.matches(keyData, "selectConfirm")) {
81
+ const selectedProvider = this.allProviders[this.selectedIndex];
82
+ if (selectedProvider) {
83
+ this.onSelectCallback(selectedProvider.id);
84
+ }
85
+ }
86
+ // Escape or Ctrl+C
87
+ else if (kb.matches(keyData, "selectCancel")) {
88
+ this.onCancelCallback();
89
+ }
90
+ }
91
+ }