indusagi-coding-agent 0.50.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,458 @@
1
+ /**
2
+ * TUI component for managing package resources (enable/disable)
3
+ */
4
+ import { basename, dirname, join, relative } from "node:path";
5
+ import { Container, getEditorKeybindings, Input, matchesKey, Spacer, truncateToWidth, visibleWidth, } from "indusagi/tui";
6
+ import { CONFIG_DIR_NAME } from "../../../config.js";
7
+ import { theme } from "../theme/theme.js";
8
+ import { DynamicBorder } from "./dynamic-border.js";
9
+ import { rawKeyHint } from "./keybinding-hints.js";
10
+ const RESOURCE_TYPE_LABELS = {
11
+ extensions: "Extensions",
12
+ skills: "Skills",
13
+ prompts: "Prompts",
14
+ themes: "Themes",
15
+ };
16
+ function getGroupLabel(metadata) {
17
+ if (metadata.origin === "package") {
18
+ return `${metadata.source} (${metadata.scope})`;
19
+ }
20
+ // Top-level resources
21
+ if (metadata.source === "auto") {
22
+ return metadata.scope === "user" ? "User (~/.indusagi/agent/)" : "Project (.indusagi/)";
23
+ }
24
+ return metadata.scope === "user" ? "User settings" : "Project settings";
25
+ }
26
+ function buildGroups(resolved) {
27
+ const groupMap = new Map();
28
+ const addToGroup = (resources, resourceType) => {
29
+ for (const res of resources) {
30
+ const { path, enabled, metadata } = res;
31
+ const groupKey = `${metadata.origin}:${metadata.scope}:${metadata.source}`;
32
+ if (!groupMap.has(groupKey)) {
33
+ groupMap.set(groupKey, {
34
+ key: groupKey,
35
+ label: getGroupLabel(metadata),
36
+ scope: metadata.scope,
37
+ origin: metadata.origin,
38
+ source: metadata.source,
39
+ subgroups: [],
40
+ });
41
+ }
42
+ const group = groupMap.get(groupKey);
43
+ const subgroupKey = `${groupKey}:${resourceType}`;
44
+ let subgroup = group.subgroups.find((sg) => sg.type === resourceType);
45
+ if (!subgroup) {
46
+ subgroup = {
47
+ type: resourceType,
48
+ label: RESOURCE_TYPE_LABELS[resourceType],
49
+ items: [],
50
+ };
51
+ group.subgroups.push(subgroup);
52
+ }
53
+ const displayName = resourceType === "skills" && basename(path) === "SKILL.md" ? basename(dirname(path)) : basename(path);
54
+ subgroup.items.push({
55
+ path,
56
+ enabled,
57
+ metadata,
58
+ resourceType,
59
+ displayName,
60
+ groupKey,
61
+ subgroupKey,
62
+ });
63
+ }
64
+ };
65
+ addToGroup(resolved.extensions, "extensions");
66
+ addToGroup(resolved.skills, "skills");
67
+ addToGroup(resolved.prompts, "prompts");
68
+ addToGroup(resolved.themes, "themes");
69
+ // Sort groups: packages first, then top-level; user before project
70
+ const groups = Array.from(groupMap.values());
71
+ groups.sort((a, b) => {
72
+ if (a.origin !== b.origin) {
73
+ return a.origin === "package" ? -1 : 1;
74
+ }
75
+ if (a.scope !== b.scope) {
76
+ return a.scope === "user" ? -1 : 1;
77
+ }
78
+ return a.source.localeCompare(b.source);
79
+ });
80
+ // Sort subgroups within each group by type order, and items by name
81
+ const typeOrder = { extensions: 0, skills: 1, prompts: 2, themes: 3 };
82
+ for (const group of groups) {
83
+ group.subgroups.sort((a, b) => typeOrder[a.type] - typeOrder[b.type]);
84
+ for (const subgroup of group.subgroups) {
85
+ subgroup.items.sort((a, b) => a.displayName.localeCompare(b.displayName));
86
+ }
87
+ }
88
+ return groups;
89
+ }
90
+ class ConfigSelectorHeader {
91
+ invalidate() { }
92
+ render(width) {
93
+ const title = theme.bold("Resource Configuration");
94
+ const sep = theme.fg("muted", " · ");
95
+ const hint = rawKeyHint("space", "toggle") + sep + rawKeyHint("esc", "close");
96
+ const hintWidth = visibleWidth(hint);
97
+ const titleWidth = visibleWidth(title);
98
+ const spacing = Math.max(1, width - titleWidth - hintWidth);
99
+ return [
100
+ truncateToWidth(`${title}${" ".repeat(spacing)}${hint}`, width, ""),
101
+ theme.fg("muted", "Type to filter resources"),
102
+ ];
103
+ }
104
+ }
105
+ class ResourceList {
106
+ get focused() {
107
+ return this._focused;
108
+ }
109
+ set focused(value) {
110
+ this._focused = value;
111
+ this.searchInput.focused = value;
112
+ }
113
+ constructor(groups, settingsManager, cwd, agentDir) {
114
+ this.flatItems = [];
115
+ this.filteredItems = [];
116
+ this.selectedIndex = 0;
117
+ this.maxVisible = 15;
118
+ this._focused = false;
119
+ this.groups = groups;
120
+ this.settingsManager = settingsManager;
121
+ this.cwd = cwd;
122
+ this.agentDir = agentDir;
123
+ this.searchInput = new Input();
124
+ this.buildFlatList();
125
+ this.filteredItems = [...this.flatItems];
126
+ }
127
+ buildFlatList() {
128
+ this.flatItems = [];
129
+ for (const group of this.groups) {
130
+ this.flatItems.push({ type: "group", group });
131
+ for (const subgroup of group.subgroups) {
132
+ this.flatItems.push({ type: "subgroup", subgroup, group });
133
+ for (const item of subgroup.items) {
134
+ this.flatItems.push({ type: "item", item });
135
+ }
136
+ }
137
+ }
138
+ // Start selection on first item (not header)
139
+ this.selectedIndex = this.flatItems.findIndex((e) => e.type === "item");
140
+ if (this.selectedIndex < 0)
141
+ this.selectedIndex = 0;
142
+ }
143
+ findNextItem(fromIndex, direction) {
144
+ let idx = fromIndex + direction;
145
+ while (idx >= 0 && idx < this.filteredItems.length) {
146
+ if (this.filteredItems[idx].type === "item") {
147
+ return idx;
148
+ }
149
+ idx += direction;
150
+ }
151
+ return fromIndex; // Stay at current if no item found
152
+ }
153
+ filterItems(query) {
154
+ if (!query.trim()) {
155
+ this.filteredItems = [...this.flatItems];
156
+ this.selectFirstItem();
157
+ return;
158
+ }
159
+ const lowerQuery = query.toLowerCase();
160
+ const matchingItems = new Set();
161
+ const matchingSubgroups = new Set();
162
+ const matchingGroups = new Set();
163
+ for (const entry of this.flatItems) {
164
+ if (entry.type === "item") {
165
+ const item = entry.item;
166
+ if (item.displayName.toLowerCase().includes(lowerQuery) ||
167
+ item.resourceType.toLowerCase().includes(lowerQuery) ||
168
+ item.path.toLowerCase().includes(lowerQuery)) {
169
+ matchingItems.add(item);
170
+ }
171
+ }
172
+ }
173
+ // Find which subgroups and groups contain matching items
174
+ for (const group of this.groups) {
175
+ for (const subgroup of group.subgroups) {
176
+ for (const item of subgroup.items) {
177
+ if (matchingItems.has(item)) {
178
+ matchingSubgroups.add(subgroup);
179
+ matchingGroups.add(group);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ this.filteredItems = [];
185
+ for (const entry of this.flatItems) {
186
+ if (entry.type === "group" && matchingGroups.has(entry.group)) {
187
+ this.filteredItems.push(entry);
188
+ }
189
+ else if (entry.type === "subgroup" && matchingSubgroups.has(entry.subgroup)) {
190
+ this.filteredItems.push(entry);
191
+ }
192
+ else if (entry.type === "item" && matchingItems.has(entry.item)) {
193
+ this.filteredItems.push(entry);
194
+ }
195
+ }
196
+ this.selectFirstItem();
197
+ }
198
+ selectFirstItem() {
199
+ const firstItemIndex = this.filteredItems.findIndex((e) => e.type === "item");
200
+ this.selectedIndex = firstItemIndex >= 0 ? firstItemIndex : 0;
201
+ }
202
+ updateItem(item, enabled) {
203
+ item.enabled = enabled;
204
+ // Update in groups too
205
+ for (const group of this.groups) {
206
+ for (const subgroup of group.subgroups) {
207
+ const found = subgroup.items.find((i) => i.path === item.path && i.resourceType === item.resourceType);
208
+ if (found) {
209
+ found.enabled = enabled;
210
+ return;
211
+ }
212
+ }
213
+ }
214
+ }
215
+ invalidate() { }
216
+ render(width) {
217
+ const lines = [];
218
+ // Search input
219
+ lines.push(...this.searchInput.render(width));
220
+ lines.push("");
221
+ if (this.filteredItems.length === 0) {
222
+ lines.push(theme.fg("muted", " No resources found"));
223
+ return lines;
224
+ }
225
+ // Calculate visible range
226
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible));
227
+ const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
228
+ for (let i = startIndex; i < endIndex; i++) {
229
+ const entry = this.filteredItems[i];
230
+ const isSelected = i === this.selectedIndex;
231
+ if (entry.type === "group") {
232
+ // Main group header (no cursor)
233
+ const groupLine = theme.fg("accent", theme.bold(entry.group.label));
234
+ lines.push(truncateToWidth(` ${groupLine}`, width, ""));
235
+ }
236
+ else if (entry.type === "subgroup") {
237
+ // Subgroup header (indented, no cursor)
238
+ const subgroupLine = theme.fg("muted", entry.subgroup.label);
239
+ lines.push(truncateToWidth(` ${subgroupLine}`, width, ""));
240
+ }
241
+ else {
242
+ // Resource item (cursor only on items)
243
+ const item = entry.item;
244
+ const cursor = isSelected ? "> " : " ";
245
+ const checkbox = item.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
246
+ const name = isSelected ? theme.bold(item.displayName) : item.displayName;
247
+ lines.push(truncateToWidth(`${cursor} ${checkbox} ${name}`, width, "..."));
248
+ }
249
+ }
250
+ // Scroll indicator
251
+ if (startIndex > 0 || endIndex < this.filteredItems.length) {
252
+ lines.push(theme.fg("dim", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`));
253
+ }
254
+ return lines;
255
+ }
256
+ handleInput(data) {
257
+ const kb = getEditorKeybindings();
258
+ if (kb.matches(data, "selectUp")) {
259
+ this.selectedIndex = this.findNextItem(this.selectedIndex, -1);
260
+ return;
261
+ }
262
+ if (kb.matches(data, "selectDown")) {
263
+ this.selectedIndex = this.findNextItem(this.selectedIndex, 1);
264
+ return;
265
+ }
266
+ if (kb.matches(data, "selectPageUp")) {
267
+ // Jump up by maxVisible, then find nearest item
268
+ let target = Math.max(0, this.selectedIndex - this.maxVisible);
269
+ while (target < this.filteredItems.length && this.filteredItems[target].type !== "item") {
270
+ target++;
271
+ }
272
+ if (target < this.filteredItems.length) {
273
+ this.selectedIndex = target;
274
+ }
275
+ return;
276
+ }
277
+ if (kb.matches(data, "selectPageDown")) {
278
+ // Jump down by maxVisible, then find nearest item
279
+ let target = Math.min(this.filteredItems.length - 1, this.selectedIndex + this.maxVisible);
280
+ while (target >= 0 && this.filteredItems[target].type !== "item") {
281
+ target--;
282
+ }
283
+ if (target >= 0) {
284
+ this.selectedIndex = target;
285
+ }
286
+ return;
287
+ }
288
+ if (kb.matches(data, "selectCancel")) {
289
+ this.onCancel?.();
290
+ return;
291
+ }
292
+ if (matchesKey(data, "ctrl+c")) {
293
+ this.onExit?.();
294
+ return;
295
+ }
296
+ if (data === " " || kb.matches(data, "selectConfirm")) {
297
+ const entry = this.filteredItems[this.selectedIndex];
298
+ if (entry?.type === "item") {
299
+ const newEnabled = !entry.item.enabled;
300
+ this.toggleResource(entry.item, newEnabled);
301
+ this.updateItem(entry.item, newEnabled);
302
+ this.onToggle?.(entry.item, newEnabled);
303
+ }
304
+ return;
305
+ }
306
+ // Pass to search input
307
+ this.searchInput.handleInput(data);
308
+ this.filterItems(this.searchInput.getValue());
309
+ }
310
+ toggleResource(item, enabled) {
311
+ if (item.metadata.origin === "top-level") {
312
+ this.toggleTopLevelResource(item, enabled);
313
+ }
314
+ else {
315
+ this.togglePackageResource(item, enabled);
316
+ }
317
+ }
318
+ toggleTopLevelResource(item, enabled) {
319
+ const scope = item.metadata.scope;
320
+ const settings = scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();
321
+ const arrayKey = item.resourceType;
322
+ const current = (settings[arrayKey] ?? []);
323
+ // Generate pattern for this resource
324
+ const pattern = this.getResourcePattern(item);
325
+ const disablePattern = `-${pattern}`;
326
+ const enablePattern = `+${pattern}`;
327
+ // Filter out existing patterns for this resource
328
+ const updated = current.filter((p) => {
329
+ const stripped = p.startsWith("!") || p.startsWith("+") || p.startsWith("-") ? p.slice(1) : p;
330
+ return stripped !== pattern;
331
+ });
332
+ if (enabled) {
333
+ updated.push(enablePattern);
334
+ }
335
+ else {
336
+ updated.push(disablePattern);
337
+ }
338
+ if (scope === "project") {
339
+ if (arrayKey === "extensions") {
340
+ this.settingsManager.setProjectExtensionPaths(updated);
341
+ }
342
+ else if (arrayKey === "skills") {
343
+ this.settingsManager.setProjectSkillPaths(updated);
344
+ }
345
+ else if (arrayKey === "prompts") {
346
+ this.settingsManager.setProjectPromptTemplatePaths(updated);
347
+ }
348
+ else if (arrayKey === "themes") {
349
+ this.settingsManager.setProjectThemePaths(updated);
350
+ }
351
+ }
352
+ else {
353
+ if (arrayKey === "extensions") {
354
+ this.settingsManager.setExtensionPaths(updated);
355
+ }
356
+ else if (arrayKey === "skills") {
357
+ this.settingsManager.setSkillPaths(updated);
358
+ }
359
+ else if (arrayKey === "prompts") {
360
+ this.settingsManager.setPromptTemplatePaths(updated);
361
+ }
362
+ else if (arrayKey === "themes") {
363
+ this.settingsManager.setThemePaths(updated);
364
+ }
365
+ }
366
+ }
367
+ togglePackageResource(item, enabled) {
368
+ const scope = item.metadata.scope;
369
+ const settings = scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();
370
+ const packages = [...(settings.packages ?? [])];
371
+ const pkgIndex = packages.findIndex((pkg) => {
372
+ const source = typeof pkg === "string" ? pkg : pkg.source;
373
+ return source === item.metadata.source;
374
+ });
375
+ if (pkgIndex === -1)
376
+ return;
377
+ let pkg = packages[pkgIndex];
378
+ // Convert string to object form if needed
379
+ if (typeof pkg === "string") {
380
+ pkg = { source: pkg };
381
+ packages[pkgIndex] = pkg;
382
+ }
383
+ // Get the resource array for this type
384
+ const arrayKey = item.resourceType;
385
+ const current = (pkg[arrayKey] ?? []);
386
+ // Generate pattern relative to package root
387
+ const pattern = this.getPackageResourcePattern(item);
388
+ const disablePattern = `-${pattern}`;
389
+ const enablePattern = `+${pattern}`;
390
+ // Filter out existing patterns for this resource
391
+ const updated = current.filter((p) => {
392
+ const stripped = p.startsWith("!") || p.startsWith("+") || p.startsWith("-") ? p.slice(1) : p;
393
+ return stripped !== pattern;
394
+ });
395
+ if (enabled) {
396
+ updated.push(enablePattern);
397
+ }
398
+ else {
399
+ updated.push(disablePattern);
400
+ }
401
+ pkg[arrayKey] = updated.length > 0 ? updated : undefined;
402
+ // Clean up empty filter object
403
+ const hasFilters = ["extensions", "skills", "prompts", "themes"].some((k) => pkg[k] !== undefined);
404
+ if (!hasFilters) {
405
+ packages[pkgIndex] = pkg.source;
406
+ }
407
+ if (scope === "project") {
408
+ this.settingsManager.setProjectPackages(packages);
409
+ }
410
+ else {
411
+ this.settingsManager.setPackages(packages);
412
+ }
413
+ }
414
+ getTopLevelBaseDir(scope) {
415
+ return scope === "project" ? join(this.cwd, CONFIG_DIR_NAME) : this.agentDir;
416
+ }
417
+ getResourcePattern(item) {
418
+ const scope = item.metadata.scope;
419
+ const baseDir = this.getTopLevelBaseDir(scope);
420
+ return relative(baseDir, item.path);
421
+ }
422
+ getPackageResourcePattern(item) {
423
+ const baseDir = item.metadata.baseDir ?? dirname(item.path);
424
+ return relative(baseDir, item.path);
425
+ }
426
+ }
427
+ export class ConfigSelectorComponent extends Container {
428
+ get focused() {
429
+ return this._focused;
430
+ }
431
+ set focused(value) {
432
+ this._focused = value;
433
+ this.resourceList.focused = value;
434
+ }
435
+ constructor(resolvedPaths, settingsManager, cwd, agentDir, onClose, onExit, requestRender) {
436
+ super();
437
+ this._focused = false;
438
+ const groups = buildGroups(resolvedPaths);
439
+ // Add header
440
+ this.addChild(new Spacer(1));
441
+ this.addChild(new DynamicBorder());
442
+ this.addChild(new Spacer(1));
443
+ this.addChild(new ConfigSelectorHeader());
444
+ this.addChild(new Spacer(1));
445
+ // Resource list
446
+ this.resourceList = new ResourceList(groups, settingsManager, cwd, agentDir);
447
+ this.resourceList.onCancel = onClose;
448
+ this.resourceList.onExit = onExit;
449
+ this.resourceList.onToggle = () => requestRender();
450
+ this.addChild(this.resourceList);
451
+ // Bottom border
452
+ this.addChild(new Spacer(1));
453
+ this.addChild(new DynamicBorder());
454
+ }
455
+ getResourceList() {
456
+ return this.resourceList;
457
+ }
458
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reusable countdown timer for dialog components.
3
+ */
4
+ export class CountdownTimer {
5
+ constructor(timeoutMs, tui, onTick, onExpire) {
6
+ this.tui = tui;
7
+ this.onTick = onTick;
8
+ this.onExpire = onExpire;
9
+ this.remainingSeconds = Math.ceil(timeoutMs / 1000);
10
+ this.onTick(this.remainingSeconds);
11
+ this.intervalId = setInterval(() => {
12
+ this.remainingSeconds--;
13
+ this.onTick(this.remainingSeconds);
14
+ this.tui?.requestRender();
15
+ if (this.remainingSeconds <= 0) {
16
+ this.dispose();
17
+ this.onExpire();
18
+ }
19
+ }, 1000);
20
+ }
21
+ dispose() {
22
+ if (this.intervalId) {
23
+ clearInterval(this.intervalId);
24
+ this.intervalId = undefined;
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,61 @@
1
+ import { Editor } from "indusagi/tui";
2
+ /**
3
+ * Custom editor that handles app-level keybindings for coding-agent.
4
+ */
5
+ export class CustomEditor extends Editor {
6
+ constructor(tui, theme, keybindings, options) {
7
+ super(tui, theme, options);
8
+ this.actionHandlers = new Map();
9
+ this.keybindings = keybindings;
10
+ }
11
+ /**
12
+ * Register a handler for an app action.
13
+ */
14
+ onAction(action, handler) {
15
+ this.actionHandlers.set(action, handler);
16
+ }
17
+ handleInput(data) {
18
+ // Check extension-registered shortcuts first
19
+ if (this.onExtensionShortcut?.(data)) {
20
+ return;
21
+ }
22
+ // Check for paste image keybinding
23
+ if (this.keybindings.matches(data, "pasteImage")) {
24
+ this.onPasteImage?.();
25
+ return;
26
+ }
27
+ // Check app keybindings first
28
+ // Escape/interrupt - only if autocomplete is NOT active
29
+ if (this.keybindings.matches(data, "interrupt")) {
30
+ if (!this.isShowingAutocomplete()) {
31
+ // Use dynamic onEscape if set, otherwise registered handler
32
+ const handler = this.onEscape ?? this.actionHandlers.get("interrupt");
33
+ if (handler) {
34
+ handler();
35
+ return;
36
+ }
37
+ }
38
+ // Let parent handle escape for autocomplete cancellation
39
+ super.handleInput(data);
40
+ return;
41
+ }
42
+ // Exit (Ctrl+D) - only when editor is empty
43
+ if (this.keybindings.matches(data, "exit")) {
44
+ if (this.getText().length === 0) {
45
+ const handler = this.onCtrlD ?? this.actionHandlers.get("exit");
46
+ if (handler)
47
+ handler();
48
+ }
49
+ return; // Always consume
50
+ }
51
+ // Check all other app actions
52
+ for (const [action, handler] of this.actionHandlers) {
53
+ if (action !== "interrupt" && action !== "exit" && this.keybindings.matches(data, action)) {
54
+ handler();
55
+ return;
56
+ }
57
+ }
58
+ // Pass to parent for editor handling
59
+ super.handleInput(data);
60
+ }
61
+ }
@@ -0,0 +1,80 @@
1
+ import { Box, Container, Markdown, Spacer, Text } from "indusagi/tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+ /**
4
+ * Component that renders a custom message entry from extensions.
5
+ * Uses distinct styling to differentiate from user messages.
6
+ */
7
+ export class CustomMessageComponent extends Container {
8
+ constructor(message, customRenderer, markdownTheme = getMarkdownTheme()) {
9
+ super();
10
+ this._expanded = false;
11
+ this.message = message;
12
+ this.customRenderer = customRenderer;
13
+ this.markdownTheme = markdownTheme;
14
+ this.addChild(new Spacer(1));
15
+ // Create box with purple background (used for default rendering)
16
+ this.box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
17
+ this.rebuild();
18
+ }
19
+ setExpanded(expanded) {
20
+ if (this._expanded !== expanded) {
21
+ this._expanded = expanded;
22
+ this.rebuild();
23
+ }
24
+ }
25
+ invalidate() {
26
+ super.invalidate();
27
+ this.rebuild();
28
+ }
29
+ rebuild() {
30
+ // Remove previous content component
31
+ if (this.customComponent) {
32
+ this.removeChild(this.customComponent);
33
+ this.customComponent = undefined;
34
+ }
35
+ this.removeChild(this.box);
36
+ // Try custom renderer first - it handles its own styling
37
+ if (this.customRenderer) {
38
+ try {
39
+ const component = this.customRenderer(this.message, { expanded: this._expanded }, theme);
40
+ if (component) {
41
+ // Custom renderer provides its own styled component
42
+ this.customComponent = component;
43
+ this.addChild(component);
44
+ return;
45
+ }
46
+ }
47
+ catch {
48
+ // Fall through to default rendering
49
+ }
50
+ }
51
+ // Default rendering uses our box
52
+ this.addChild(this.box);
53
+ this.box.clear();
54
+ // Default rendering: label + content
55
+ const label = theme.fg("customMessageLabel", `\x1b[1m[${this.message.customType}]\x1b[22m`);
56
+ this.box.addChild(new Text(label, 0, 0));
57
+ this.box.addChild(new Spacer(1));
58
+ // Extract text content
59
+ let text;
60
+ if (typeof this.message.content === "string") {
61
+ text = this.message.content;
62
+ }
63
+ else {
64
+ text = this.message.content
65
+ .filter((c) => c.type === "text")
66
+ .map((c) => c.text)
67
+ .join("\n");
68
+ }
69
+ // Limit lines when collapsed
70
+ if (!this._expanded) {
71
+ const lines = text.split("\n");
72
+ if (lines.length > 5) {
73
+ text = `${lines.slice(0, 5).join("\n")}\n...`;
74
+ }
75
+ }
76
+ this.box.addChild(new Markdown(text, 0, 0, this.markdownTheme, {
77
+ color: (text) => theme.fg("customMessageText", text),
78
+ }));
79
+ }
80
+ }