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,262 @@
1
+ import { Container, fuzzyFilter, getEditorKeybindings, Input, Key, matchesKey, Spacer, Text, } from "indusagi/tui";
2
+ import { theme } from "../theme/theme.js";
3
+ import { DynamicBorder } from "./dynamic-border.js";
4
+ function isEnabled(enabledIds, id) {
5
+ return enabledIds === null || enabledIds.includes(id);
6
+ }
7
+ function toggle(enabledIds, id) {
8
+ if (enabledIds === null)
9
+ return [id]; // First toggle: start with only this one
10
+ const index = enabledIds.indexOf(id);
11
+ if (index >= 0)
12
+ return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)];
13
+ return [...enabledIds, id];
14
+ }
15
+ function enableAll(enabledIds, allIds, targetIds) {
16
+ if (enabledIds === null)
17
+ return null; // Already all enabled
18
+ const targets = targetIds ?? allIds;
19
+ const result = [...enabledIds];
20
+ for (const id of targets) {
21
+ if (!result.includes(id))
22
+ result.push(id);
23
+ }
24
+ return result.length === allIds.length ? null : result;
25
+ }
26
+ function clearAll(enabledIds, allIds, targetIds) {
27
+ if (enabledIds === null) {
28
+ return targetIds ? allIds.filter((id) => !targetIds.includes(id)) : [];
29
+ }
30
+ const targets = new Set(targetIds ?? enabledIds);
31
+ return enabledIds.filter((id) => !targets.has(id));
32
+ }
33
+ function move(enabledIds, allIds, id, delta) {
34
+ const list = enabledIds ?? [...allIds];
35
+ const index = list.indexOf(id);
36
+ if (index < 0)
37
+ return list;
38
+ const newIndex = index + delta;
39
+ if (newIndex < 0 || newIndex >= list.length)
40
+ return list;
41
+ const result = [...list];
42
+ [result[index], result[newIndex]] = [result[newIndex], result[index]];
43
+ return result;
44
+ }
45
+ function getSortedIds(enabledIds, allIds) {
46
+ if (enabledIds === null)
47
+ return allIds;
48
+ const enabledSet = new Set(enabledIds);
49
+ return [...enabledIds, ...allIds.filter((id) => !enabledSet.has(id))];
50
+ }
51
+ /**
52
+ * Component for enabling/disabling models for Ctrl+P cycling.
53
+ * Changes are session-only until explicitly persisted with Ctrl+S.
54
+ */
55
+ export class ScopedModelsSelectorComponent extends Container {
56
+ get focused() {
57
+ return this._focused;
58
+ }
59
+ set focused(value) {
60
+ this._focused = value;
61
+ this.searchInput.focused = value;
62
+ }
63
+ constructor(config, callbacks) {
64
+ super();
65
+ this.modelsById = new Map();
66
+ this.allIds = [];
67
+ this.enabledIds = null;
68
+ this.filteredItems = [];
69
+ this.selectedIndex = 0;
70
+ // Focusable implementation - propagate to searchInput for IME cursor positioning
71
+ this._focused = false;
72
+ this.maxVisible = 15;
73
+ this.isDirty = false;
74
+ this.callbacks = callbacks;
75
+ for (const model of config.allModels) {
76
+ const fullId = `${model.provider}/${model.id}`;
77
+ this.modelsById.set(fullId, model);
78
+ this.allIds.push(fullId);
79
+ }
80
+ this.enabledIds = config.hasEnabledModelsFilter ? [...config.enabledModelIds] : null;
81
+ this.filteredItems = this.buildItems();
82
+ // Header
83
+ this.addChild(new DynamicBorder());
84
+ this.addChild(new Spacer(1));
85
+ this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0));
86
+ this.addChild(new Text(theme.fg("muted", "Session-only. Ctrl+S to save to settings."), 0, 0));
87
+ this.addChild(new Spacer(1));
88
+ // Search input
89
+ this.searchInput = new Input();
90
+ this.addChild(this.searchInput);
91
+ this.addChild(new Spacer(1));
92
+ // List container
93
+ this.listContainer = new Container();
94
+ this.addChild(this.listContainer);
95
+ // Footer hint
96
+ this.addChild(new Spacer(1));
97
+ this.footerText = new Text(this.getFooterText(), 0, 0);
98
+ this.addChild(this.footerText);
99
+ this.addChild(new DynamicBorder());
100
+ this.updateList();
101
+ }
102
+ buildItems() {
103
+ return getSortedIds(this.enabledIds, this.allIds).map((id) => ({
104
+ fullId: id,
105
+ model: this.modelsById.get(id),
106
+ enabled: isEnabled(this.enabledIds, id),
107
+ }));
108
+ }
109
+ getFooterText() {
110
+ const enabledCount = this.enabledIds?.length ?? this.allIds.length;
111
+ const allEnabled = this.enabledIds === null;
112
+ const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.allIds.length} enabled`;
113
+ const parts = ["Enter toggle", "^A all", "^X clear", "^P provider", "Alt+↑↓ reorder", "^S save", countText];
114
+ return this.isDirty
115
+ ? theme.fg("dim", ` ${parts.join(" · ")} `) + theme.fg("warning", "(unsaved)")
116
+ : theme.fg("dim", ` ${parts.join(" · ")}`);
117
+ }
118
+ refresh() {
119
+ const query = this.searchInput.getValue();
120
+ const items = this.buildItems();
121
+ this.filteredItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items;
122
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));
123
+ this.updateList();
124
+ this.footerText.setText(this.getFooterText());
125
+ }
126
+ updateList() {
127
+ this.listContainer.clear();
128
+ if (this.filteredItems.length === 0) {
129
+ this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
130
+ return;
131
+ }
132
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible));
133
+ const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
134
+ const allEnabled = this.enabledIds === null;
135
+ for (let i = startIndex; i < endIndex; i++) {
136
+ const item = this.filteredItems[i];
137
+ const isSelected = i === this.selectedIndex;
138
+ const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
139
+ const modelText = isSelected ? theme.fg("accent", item.model.id) : item.model.id;
140
+ const providerBadge = theme.fg("muted", ` [${item.model.provider}]`);
141
+ const status = allEnabled ? "" : item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗");
142
+ this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0));
143
+ }
144
+ // Add scroll indicator if needed
145
+ if (startIndex > 0 || endIndex < this.filteredItems.length) {
146
+ this.listContainer.addChild(new Text(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`), 0, 0));
147
+ }
148
+ }
149
+ handleInput(data) {
150
+ const kb = getEditorKeybindings();
151
+ // Navigation
152
+ if (kb.matches(data, "selectUp")) {
153
+ if (this.filteredItems.length === 0)
154
+ return;
155
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
156
+ this.updateList();
157
+ return;
158
+ }
159
+ if (kb.matches(data, "selectDown")) {
160
+ if (this.filteredItems.length === 0)
161
+ return;
162
+ this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
163
+ this.updateList();
164
+ return;
165
+ }
166
+ // Alt+Up/Down - Reorder enabled models
167
+ if (matchesKey(data, Key.alt("up")) || matchesKey(data, Key.alt("down"))) {
168
+ const item = this.filteredItems[this.selectedIndex];
169
+ if (item && isEnabled(this.enabledIds, item.fullId)) {
170
+ const delta = matchesKey(data, Key.alt("up")) ? -1 : 1;
171
+ const enabledList = this.enabledIds ?? this.allIds;
172
+ const currentIndex = enabledList.indexOf(item.fullId);
173
+ const newIndex = currentIndex + delta;
174
+ // Only move if within bounds
175
+ if (newIndex >= 0 && newIndex < enabledList.length) {
176
+ this.enabledIds = move(this.enabledIds, this.allIds, item.fullId, delta);
177
+ this.isDirty = true;
178
+ this.selectedIndex += delta;
179
+ this.refresh();
180
+ }
181
+ }
182
+ return;
183
+ }
184
+ // Toggle on Enter
185
+ if (matchesKey(data, Key.enter)) {
186
+ const item = this.filteredItems[this.selectedIndex];
187
+ if (item) {
188
+ const wasAllEnabled = this.enabledIds === null;
189
+ this.enabledIds = toggle(this.enabledIds, item.fullId);
190
+ this.isDirty = true;
191
+ if (wasAllEnabled)
192
+ this.callbacks.onClearAll();
193
+ this.callbacks.onModelToggle(item.fullId, isEnabled(this.enabledIds, item.fullId));
194
+ this.refresh();
195
+ }
196
+ return;
197
+ }
198
+ // Ctrl+A - Enable all (filtered if search active, otherwise all)
199
+ if (matchesKey(data, Key.ctrl("a"))) {
200
+ const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;
201
+ this.enabledIds = enableAll(this.enabledIds, this.allIds, targetIds);
202
+ this.isDirty = true;
203
+ this.callbacks.onEnableAll(targetIds ?? this.allIds);
204
+ this.refresh();
205
+ return;
206
+ }
207
+ // Ctrl+X - Clear all (filtered if search active, otherwise all)
208
+ if (matchesKey(data, Key.ctrl("x"))) {
209
+ const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;
210
+ this.enabledIds = clearAll(this.enabledIds, this.allIds, targetIds);
211
+ this.isDirty = true;
212
+ this.callbacks.onClearAll();
213
+ this.refresh();
214
+ return;
215
+ }
216
+ // Ctrl+P - Toggle provider of current item
217
+ if (matchesKey(data, Key.ctrl("p"))) {
218
+ const item = this.filteredItems[this.selectedIndex];
219
+ if (item) {
220
+ const provider = item.model.provider;
221
+ const providerIds = this.allIds.filter((id) => this.modelsById.get(id).provider === provider);
222
+ const allEnabled = providerIds.every((id) => isEnabled(this.enabledIds, id));
223
+ this.enabledIds = allEnabled
224
+ ? clearAll(this.enabledIds, this.allIds, providerIds)
225
+ : enableAll(this.enabledIds, this.allIds, providerIds);
226
+ this.isDirty = true;
227
+ this.callbacks.onToggleProvider(provider, providerIds, !allEnabled);
228
+ this.refresh();
229
+ }
230
+ return;
231
+ }
232
+ // Ctrl+S - Save/persist to settings
233
+ if (matchesKey(data, Key.ctrl("s"))) {
234
+ this.callbacks.onPersist(this.enabledIds ?? [...this.allIds]);
235
+ this.isDirty = false;
236
+ this.footerText.setText(this.getFooterText());
237
+ return;
238
+ }
239
+ // Ctrl+C - clear search or cancel if empty
240
+ if (matchesKey(data, Key.ctrl("c"))) {
241
+ if (this.searchInput.getValue()) {
242
+ this.searchInput.setValue("");
243
+ this.refresh();
244
+ }
245
+ else {
246
+ this.callbacks.onCancel();
247
+ }
248
+ return;
249
+ }
250
+ // Escape - cancel
251
+ if (matchesKey(data, Key.escape)) {
252
+ this.callbacks.onCancel();
253
+ return;
254
+ }
255
+ // Pass everything else to search input
256
+ this.searchInput.handleInput(data);
257
+ this.refresh();
258
+ }
259
+ getSearchInput() {
260
+ return this.searchInput;
261
+ }
262
+ }
@@ -0,0 +1,145 @@
1
+ import { fuzzyMatch } from "indusagi/tui";
2
+ function normalizeWhitespaceLower(text) {
3
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
4
+ }
5
+ function getSessionSearchText(session) {
6
+ return `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`;
7
+ }
8
+ export function parseSearchQuery(query) {
9
+ const trimmed = query.trim();
10
+ if (!trimmed) {
11
+ return { mode: "tokens", tokens: [], regex: null };
12
+ }
13
+ // Regex mode: re:<pattern>
14
+ if (trimmed.startsWith("re:")) {
15
+ const pattern = trimmed.slice(3).trim();
16
+ if (!pattern) {
17
+ return { mode: "regex", tokens: [], regex: null, error: "Empty regex" };
18
+ }
19
+ try {
20
+ return { mode: "regex", tokens: [], regex: new RegExp(pattern, "i") };
21
+ }
22
+ catch (err) {
23
+ const msg = err instanceof Error ? err.message : String(err);
24
+ return { mode: "regex", tokens: [], regex: null, error: msg };
25
+ }
26
+ }
27
+ // Token mode with quote support.
28
+ // Example: foo "node cve" bar
29
+ const tokens = [];
30
+ let buf = "";
31
+ let inQuote = false;
32
+ let hadUnclosedQuote = false;
33
+ const flush = (kind) => {
34
+ const v = buf.trim();
35
+ buf = "";
36
+ if (!v)
37
+ return;
38
+ tokens.push({ kind, value: v });
39
+ };
40
+ for (let i = 0; i < trimmed.length; i++) {
41
+ const ch = trimmed[i];
42
+ if (ch === '"') {
43
+ if (inQuote) {
44
+ flush("phrase");
45
+ inQuote = false;
46
+ }
47
+ else {
48
+ flush("fuzzy");
49
+ inQuote = true;
50
+ }
51
+ continue;
52
+ }
53
+ if (!inQuote && /\s/.test(ch)) {
54
+ flush("fuzzy");
55
+ continue;
56
+ }
57
+ buf += ch;
58
+ }
59
+ if (inQuote) {
60
+ hadUnclosedQuote = true;
61
+ }
62
+ // If quotes were unbalanced, fall back to plain whitespace tokenization.
63
+ if (hadUnclosedQuote) {
64
+ return {
65
+ mode: "tokens",
66
+ tokens: trimmed
67
+ .split(/\s+/)
68
+ .map((t) => t.trim())
69
+ .filter((t) => t.length > 0)
70
+ .map((t) => ({ kind: "fuzzy", value: t })),
71
+ regex: null,
72
+ };
73
+ }
74
+ flush(inQuote ? "phrase" : "fuzzy");
75
+ return { mode: "tokens", tokens, regex: null };
76
+ }
77
+ export function matchSession(session, parsed) {
78
+ const text = getSessionSearchText(session);
79
+ if (parsed.mode === "regex") {
80
+ if (!parsed.regex) {
81
+ return { matches: false, score: 0 };
82
+ }
83
+ const idx = text.search(parsed.regex);
84
+ if (idx < 0)
85
+ return { matches: false, score: 0 };
86
+ return { matches: true, score: idx * 0.1 };
87
+ }
88
+ if (parsed.tokens.length === 0) {
89
+ return { matches: true, score: 0 };
90
+ }
91
+ let totalScore = 0;
92
+ let normalizedText = null;
93
+ for (const token of parsed.tokens) {
94
+ if (token.kind === "phrase") {
95
+ if (normalizedText === null) {
96
+ normalizedText = normalizeWhitespaceLower(text);
97
+ }
98
+ const phrase = normalizeWhitespaceLower(token.value);
99
+ if (!phrase)
100
+ continue;
101
+ const idx = normalizedText.indexOf(phrase);
102
+ if (idx < 0)
103
+ return { matches: false, score: 0 };
104
+ totalScore += idx * 0.1;
105
+ continue;
106
+ }
107
+ const m = fuzzyMatch(token.value, text);
108
+ if (!m.matches)
109
+ return { matches: false, score: 0 };
110
+ totalScore += m.score;
111
+ }
112
+ return { matches: true, score: totalScore };
113
+ }
114
+ export function filterAndSortSessions(sessions, query, sortMode) {
115
+ const trimmed = query.trim();
116
+ if (!trimmed)
117
+ return sessions;
118
+ const parsed = parseSearchQuery(query);
119
+ if (parsed.error)
120
+ return [];
121
+ // Recent mode: filter only, keep incoming order.
122
+ if (sortMode === "recent") {
123
+ const filtered = [];
124
+ for (const s of sessions) {
125
+ const res = matchSession(s, parsed);
126
+ if (res.matches)
127
+ filtered.push(s);
128
+ }
129
+ return filtered;
130
+ }
131
+ // Relevance mode: sort by score, tie-break by modified desc.
132
+ const scored = [];
133
+ for (const s of sessions) {
134
+ const res = matchSession(s, parsed);
135
+ if (!res.matches)
136
+ continue;
137
+ scored.push({ session: s, score: res.score });
138
+ }
139
+ scored.sort((a, b) => {
140
+ if (a.score !== b.score)
141
+ return a.score - b.score;
142
+ return b.session.modified.getTime() - a.session.modified.getTime();
143
+ });
144
+ return scored.map((r) => r.session);
145
+ }