@dungle-scrubs/tallow 0.8.21 → 0.8.23

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 (217) hide show
  1. package/dist/cli.js +35 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +2 -0
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +82 -0
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/sdk.d.ts +17 -0
  10. package/dist/sdk.d.ts.map +1 -1
  11. package/dist/sdk.js +68 -1
  12. package/dist/sdk.js.map +1 -1
  13. package/dist/workspace-transition-relay.d.ts +40 -7
  14. package/dist/workspace-transition-relay.d.ts.map +1 -1
  15. package/dist/workspace-transition-relay.js +81 -16
  16. package/dist/workspace-transition-relay.js.map +1 -1
  17. package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
  18. package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
  19. package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
  20. package/extensions/_shared/atomic-write.ts +1 -1
  21. package/extensions/_shared/bordered-box.ts +102 -0
  22. package/extensions/_shared/interop-events.ts +5 -0
  23. package/extensions/_shared/pid-registry.ts +1 -1
  24. package/extensions/agent-commands-tool/index.ts +4 -1
  25. package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
  26. package/extensions/background-task-tool/index.ts +139 -221
  27. package/extensions/bash-tool-enhanced/index.ts +1 -75
  28. package/extensions/cd-tool/index.ts +2 -2
  29. package/extensions/context-fork/spawn.ts +4 -1
  30. package/extensions/health/index.ts +6 -6
  31. package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
  32. package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
  33. package/extensions/hooks/index.ts +27 -4
  34. package/extensions/loop/__tests__/loop.test.ts +168 -4
  35. package/extensions/loop/extension.json +6 -5
  36. package/extensions/loop/index.ts +242 -31
  37. package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
  38. package/extensions/plan-mode-tool/index.ts +103 -41
  39. package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
  40. package/extensions/prompt-suggestions/index.ts +41 -6
  41. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
  42. package/extensions/slash-command-bridge/extension.json +1 -1
  43. package/extensions/slash-command-bridge/index.ts +230 -116
  44. package/extensions/subagent-tool/index.ts +2 -2
  45. package/extensions/subagent-tool/process.ts +4 -5
  46. package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
  47. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
  48. package/extensions/teams-tool/dashboard.ts +3 -5
  49. package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
  50. package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
  51. package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
  52. package/extensions/wezterm-pane-control/index.ts +113 -8
  53. package/package.json +6 -4
  54. package/packages/tallow-tui/README.md +51 -0
  55. package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
  56. package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
  57. package/packages/tallow-tui/dist/autocomplete.js +564 -0
  58. package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
  59. package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
  60. package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
  61. package/packages/tallow-tui/dist/border-styles.js +46 -0
  62. package/packages/tallow-tui/dist/border-styles.js.map +1 -0
  63. package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
  64. package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
  65. package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
  66. package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
  67. package/packages/tallow-tui/dist/components/box.d.ts +22 -0
  68. package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
  69. package/packages/tallow-tui/dist/components/box.js +104 -0
  70. package/packages/tallow-tui/dist/components/box.js.map +1 -0
  71. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
  72. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
  73. package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
  74. package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
  75. package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
  76. package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
  77. package/packages/tallow-tui/dist/components/editor.js +1766 -0
  78. package/packages/tallow-tui/dist/components/editor.js.map +1 -0
  79. package/packages/tallow-tui/dist/components/image.d.ts +126 -0
  80. package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
  81. package/packages/tallow-tui/dist/components/image.js +245 -0
  82. package/packages/tallow-tui/dist/components/image.js.map +1 -0
  83. package/packages/tallow-tui/dist/components/input.d.ts +37 -0
  84. package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
  85. package/packages/tallow-tui/dist/components/input.js +439 -0
  86. package/packages/tallow-tui/dist/components/input.js.map +1 -0
  87. package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
  88. package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
  89. package/packages/tallow-tui/dist/components/loader.js +146 -0
  90. package/packages/tallow-tui/dist/components/loader.js.map +1 -0
  91. package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
  92. package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
  93. package/packages/tallow-tui/dist/components/markdown.js +633 -0
  94. package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
  95. package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
  96. package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
  97. package/packages/tallow-tui/dist/components/select-list.js +156 -0
  98. package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
  99. package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
  100. package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
  101. package/packages/tallow-tui/dist/components/settings-list.js +189 -0
  102. package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
  103. package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
  104. package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
  105. package/packages/tallow-tui/dist/components/spacer.js +23 -0
  106. package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
  107. package/packages/tallow-tui/dist/components/text.d.ts +19 -0
  108. package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
  109. package/packages/tallow-tui/dist/components/text.js +91 -0
  110. package/packages/tallow-tui/dist/components/text.js.map +1 -0
  111. package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
  112. package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
  113. package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
  114. package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
  115. package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
  116. package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
  117. package/packages/tallow-tui/dist/editor-component.js +2 -0
  118. package/packages/tallow-tui/dist/editor-component.js.map +1 -0
  119. package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
  120. package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
  121. package/packages/tallow-tui/dist/fuzzy.js +107 -0
  122. package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
  123. package/packages/tallow-tui/dist/index.d.ts +25 -0
  124. package/packages/tallow-tui/dist/index.d.ts.map +1 -0
  125. package/packages/tallow-tui/dist/index.js +35 -0
  126. package/packages/tallow-tui/dist/index.js.map +1 -0
  127. package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
  128. package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
  129. package/packages/tallow-tui/dist/keybindings.js +114 -0
  130. package/packages/tallow-tui/dist/keybindings.js.map +1 -0
  131. package/packages/tallow-tui/dist/keys.d.ts +168 -0
  132. package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
  133. package/packages/tallow-tui/dist/keys.js +971 -0
  134. package/packages/tallow-tui/dist/keys.js.map +1 -0
  135. package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
  136. package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
  137. package/packages/tallow-tui/dist/kill-ring.js +44 -0
  138. package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
  139. package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
  140. package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
  141. package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
  142. package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
  143. package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
  144. package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
  145. package/packages/tallow-tui/dist/terminal-image.js +460 -0
  146. package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
  147. package/packages/tallow-tui/dist/terminal.d.ts +102 -0
  148. package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
  149. package/packages/tallow-tui/dist/terminal.js +263 -0
  150. package/packages/tallow-tui/dist/terminal.js.map +1 -0
  151. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
  152. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
  153. package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
  154. package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
  155. package/packages/tallow-tui/dist/tui.d.ts +239 -0
  156. package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
  157. package/packages/tallow-tui/dist/tui.js +1058 -0
  158. package/packages/tallow-tui/dist/tui.js.map +1 -0
  159. package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
  160. package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
  161. package/packages/tallow-tui/dist/undo-stack.js +25 -0
  162. package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
  163. package/packages/tallow-tui/dist/utils.d.ts +96 -0
  164. package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
  165. package/packages/tallow-tui/dist/utils.js +843 -0
  166. package/packages/tallow-tui/dist/utils.js.map +1 -0
  167. package/packages/tallow-tui/package.json +24 -0
  168. package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
  169. package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
  170. package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
  171. package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
  172. package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
  173. package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
  174. package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
  175. package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
  176. package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
  177. package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
  178. package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
  179. package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
  180. package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
  181. package/packages/tallow-tui/src/autocomplete.ts +716 -0
  182. package/packages/tallow-tui/src/border-styles.ts +60 -0
  183. package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
  184. package/packages/tallow-tui/src/components/box.ts +137 -0
  185. package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
  186. package/packages/tallow-tui/src/components/editor.ts +2143 -0
  187. package/packages/tallow-tui/src/components/image.ts +315 -0
  188. package/packages/tallow-tui/src/components/input.ts +522 -0
  189. package/packages/tallow-tui/src/components/loader.ts +187 -0
  190. package/packages/tallow-tui/src/components/markdown.ts +780 -0
  191. package/packages/tallow-tui/src/components/select-list.ts +197 -0
  192. package/packages/tallow-tui/src/components/settings-list.ts +264 -0
  193. package/packages/tallow-tui/src/components/spacer.ts +28 -0
  194. package/packages/tallow-tui/src/components/text.ts +113 -0
  195. package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
  196. package/packages/tallow-tui/src/editor-component.ts +92 -0
  197. package/packages/tallow-tui/src/fuzzy.ts +133 -0
  198. package/packages/tallow-tui/src/index.ts +118 -0
  199. package/packages/tallow-tui/src/keybindings.ts +183 -0
  200. package/packages/tallow-tui/src/keys.ts +1189 -0
  201. package/packages/tallow-tui/src/kill-ring.ts +46 -0
  202. package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
  203. package/packages/tallow-tui/src/terminal-image.ts +619 -0
  204. package/packages/tallow-tui/src/terminal.ts +350 -0
  205. package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
  206. package/packages/tallow-tui/src/tui.ts +1336 -0
  207. package/packages/tallow-tui/src/undo-stack.ts +28 -0
  208. package/packages/tallow-tui/src/utils.ts +948 -0
  209. package/packages/tallow-tui/tsconfig.build.json +21 -0
  210. package/runtime/agent-runner.ts +20 -0
  211. package/runtime/atomic-write.ts +8 -0
  212. package/runtime/otel.ts +12 -0
  213. package/runtime/resolve-module.ts +23 -0
  214. package/runtime/runtime-path-provider.ts +12 -0
  215. package/runtime/runtime-provenance.ts +17 -0
  216. package/runtime/workspace-transition-relay.ts +21 -0
  217. package/runtime/workspace-transition.ts +29 -0
@@ -0,0 +1,197 @@
1
+ import { getEditorKeybindings } from "../keybindings.js";
2
+ import type { Component } from "../tui.js";
3
+ import { truncateToWidth } from "../utils.js";
4
+
5
+ const normalizeToSingleLine = (text: string): string => text.replace(/[\r\n]+/g, " ").trim();
6
+
7
+ export interface SelectItem {
8
+ value: string;
9
+ label: string;
10
+ description?: string;
11
+ }
12
+
13
+ export interface SelectListTheme {
14
+ selectedPrefix: (text: string) => string;
15
+ selectedText: (text: string) => string;
16
+ description: (text: string) => string;
17
+ scrollInfo: (text: string) => string;
18
+ noMatch: (text: string) => string;
19
+ }
20
+
21
+ export class SelectList implements Component {
22
+ private items: SelectItem[] = [];
23
+ private filteredItems: SelectItem[] = [];
24
+ private selectedIndex: number = 0;
25
+ private maxVisible: number = 5;
26
+ private theme: SelectListTheme;
27
+
28
+ public onSelect?: (item: SelectItem) => void;
29
+ public onCancel?: () => void;
30
+ public onSelectionChange?: (item: SelectItem) => void;
31
+
32
+ constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme) {
33
+ this.items = items;
34
+ this.filteredItems = items;
35
+ this.maxVisible = maxVisible;
36
+ this.theme = theme;
37
+ }
38
+
39
+ setFilter(filter: string): void {
40
+ this.filteredItems = this.items.filter((item) =>
41
+ item.value.toLowerCase().startsWith(filter.toLowerCase())
42
+ );
43
+ // Reset selection when filter changes
44
+ this.selectedIndex = 0;
45
+ }
46
+
47
+ setSelectedIndex(index: number): void {
48
+ this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));
49
+ }
50
+
51
+ invalidate(): void {
52
+ // No cached state to invalidate currently
53
+ }
54
+
55
+ render(width: number): string[] {
56
+ const lines: string[] = [];
57
+
58
+ // If no items match filter, show message
59
+ if (this.filteredItems.length === 0) {
60
+ lines.push(this.theme.noMatch(" No matching commands"));
61
+ return lines;
62
+ }
63
+
64
+ // Calculate visible range with scrolling
65
+ const startIndex = Math.max(
66
+ 0,
67
+ Math.min(
68
+ this.selectedIndex - Math.floor(this.maxVisible / 2),
69
+ this.filteredItems.length - this.maxVisible
70
+ )
71
+ );
72
+ const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
73
+
74
+ // Render visible items
75
+ for (let i = startIndex; i < endIndex; i++) {
76
+ const item = this.filteredItems[i];
77
+ if (!item) continue;
78
+
79
+ const isSelected = i === this.selectedIndex;
80
+ const descriptionSingleLine = item.description
81
+ ? normalizeToSingleLine(item.description)
82
+ : undefined;
83
+
84
+ let line = "";
85
+ if (isSelected) {
86
+ // Use arrow indicator for selection - entire line uses selectedText color
87
+ const prefixWidth = 2; // "↗ " is 2 characters visually
88
+ const displayValue = item.label || item.value;
89
+
90
+ if (descriptionSingleLine && width > 40) {
91
+ // Calculate how much space we have for value + description
92
+ const maxValueWidth = Math.min(30, width - prefixWidth - 4);
93
+ const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
94
+ const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length));
95
+
96
+ // Calculate remaining space for description using visible widths
97
+ const descriptionStart = prefixWidth + truncatedValue.length + spacing.length;
98
+ const remainingWidth = width - descriptionStart - 2; // -2 for safety
99
+
100
+ if (remainingWidth > 10) {
101
+ const truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, "");
102
+ // Apply selectedText to entire line content
103
+ line = this.theme.selectedText(`↗ ${truncatedValue}${spacing}${truncatedDesc}`);
104
+ } else {
105
+ // Not enough space for description
106
+ const maxWidth = width - prefixWidth - 2;
107
+ line = this.theme.selectedText(`↗ ${truncateToWidth(displayValue, maxWidth, "")}`);
108
+ }
109
+ } else {
110
+ // No description or not enough width
111
+ const maxWidth = width - prefixWidth - 2;
112
+ line = this.theme.selectedText(`↗ ${truncateToWidth(displayValue, maxWidth, "")}`);
113
+ }
114
+ } else {
115
+ const displayValue = item.label || item.value;
116
+ const prefix = " ";
117
+
118
+ if (descriptionSingleLine && width > 40) {
119
+ // Calculate how much space we have for value + description
120
+ const maxValueWidth = Math.min(30, width - prefix.length - 4);
121
+ const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
122
+ const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length));
123
+
124
+ // Calculate remaining space for description
125
+ const descriptionStart = prefix.length + truncatedValue.length + spacing.length;
126
+ const remainingWidth = width - descriptionStart - 2; // -2 for safety
127
+
128
+ if (remainingWidth > 10) {
129
+ const truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, "");
130
+ const descText = this.theme.description(spacing + truncatedDesc);
131
+ line = prefix + truncatedValue + descText;
132
+ } else {
133
+ // Not enough space for description
134
+ const maxWidth = width - prefix.length - 2;
135
+ line = prefix + truncateToWidth(displayValue, maxWidth, "");
136
+ }
137
+ } else {
138
+ // No description or not enough width
139
+ const maxWidth = width - prefix.length - 2;
140
+ line = prefix + truncateToWidth(displayValue, maxWidth, "");
141
+ }
142
+ }
143
+
144
+ lines.push(line);
145
+ }
146
+
147
+ // Add scroll indicators if needed
148
+ if (startIndex > 0 || endIndex < this.filteredItems.length) {
149
+ const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;
150
+ // Truncate if too long for terminal
151
+ lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")));
152
+ }
153
+
154
+ return lines;
155
+ }
156
+
157
+ handleInput(keyData: string): void {
158
+ const kb = getEditorKeybindings();
159
+ // Up arrow - wrap to bottom when at top
160
+ if (kb.matches(keyData, "selectUp")) {
161
+ this.selectedIndex =
162
+ this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
163
+ this.notifySelectionChange();
164
+ }
165
+ // Down arrow - wrap to top when at bottom
166
+ else if (kb.matches(keyData, "selectDown")) {
167
+ this.selectedIndex =
168
+ this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
169
+ this.notifySelectionChange();
170
+ }
171
+ // Enter
172
+ else if (kb.matches(keyData, "selectConfirm")) {
173
+ const selectedItem = this.filteredItems[this.selectedIndex];
174
+ if (selectedItem && this.onSelect) {
175
+ this.onSelect(selectedItem);
176
+ }
177
+ }
178
+ // Escape or Ctrl+C
179
+ else if (kb.matches(keyData, "selectCancel")) {
180
+ if (this.onCancel) {
181
+ this.onCancel();
182
+ }
183
+ }
184
+ }
185
+
186
+ private notifySelectionChange(): void {
187
+ const selectedItem = this.filteredItems[this.selectedIndex];
188
+ if (selectedItem && this.onSelectionChange) {
189
+ this.onSelectionChange(selectedItem);
190
+ }
191
+ }
192
+
193
+ getSelectedItem(): SelectItem | null {
194
+ const item = this.filteredItems[this.selectedIndex];
195
+ return item || null;
196
+ }
197
+ }
@@ -0,0 +1,264 @@
1
+ import { fuzzyFilter } from "../fuzzy.js";
2
+ import { getEditorKeybindings } from "../keybindings.js";
3
+ import type { Component } from "../tui.js";
4
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils.js";
5
+ import { Input } from "./input.js";
6
+
7
+ export interface SettingItem {
8
+ /** Unique identifier for this setting */
9
+ id: string;
10
+ /** Display label (left side) */
11
+ label: string;
12
+ /** Optional description shown when selected */
13
+ description?: string;
14
+ /** Current value to display (right side) */
15
+ currentValue: string;
16
+ /** If provided, Enter/Space cycles through these values */
17
+ values?: string[];
18
+ /** If provided, Enter opens this submenu. Receives current value and done callback. */
19
+ submenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;
20
+ }
21
+
22
+ export interface SettingsListTheme {
23
+ label: (text: string, selected: boolean) => string;
24
+ value: (text: string, selected: boolean) => string;
25
+ description: (text: string) => string;
26
+ cursor: string;
27
+ hint: (text: string) => string;
28
+ }
29
+
30
+ export interface SettingsListOptions {
31
+ enableSearch?: boolean;
32
+ }
33
+
34
+ export class SettingsList implements Component {
35
+ private items: SettingItem[];
36
+ private filteredItems: SettingItem[];
37
+ private theme: SettingsListTheme;
38
+ private selectedIndex = 0;
39
+ private maxVisible: number;
40
+ private onChange: (id: string, newValue: string) => void;
41
+ private onCancel: () => void;
42
+ private searchInput?: Input;
43
+ private searchEnabled: boolean;
44
+
45
+ // Submenu state
46
+ private submenuComponent: Component | null = null;
47
+ private submenuItemIndex: number | null = null;
48
+
49
+ constructor(
50
+ items: SettingItem[],
51
+ maxVisible: number,
52
+ theme: SettingsListTheme,
53
+ onChange: (id: string, newValue: string) => void,
54
+ onCancel: () => void,
55
+ options: SettingsListOptions = {}
56
+ ) {
57
+ this.items = items;
58
+ this.filteredItems = items;
59
+ this.maxVisible = maxVisible;
60
+ this.theme = theme;
61
+ this.onChange = onChange;
62
+ this.onCancel = onCancel;
63
+ this.searchEnabled = options.enableSearch ?? false;
64
+ if (this.searchEnabled) {
65
+ this.searchInput = new Input();
66
+ }
67
+ }
68
+
69
+ /** Update an item's currentValue */
70
+ updateValue(id: string, newValue: string): void {
71
+ const item = this.items.find((i) => i.id === id);
72
+ if (item) {
73
+ item.currentValue = newValue;
74
+ }
75
+ }
76
+
77
+ invalidate(): void {
78
+ this.submenuComponent?.invalidate?.();
79
+ }
80
+
81
+ render(width: number): string[] {
82
+ // If submenu is active, render it instead
83
+ if (this.submenuComponent) {
84
+ return this.submenuComponent.render(width);
85
+ }
86
+
87
+ return this.renderMainList(width);
88
+ }
89
+
90
+ private renderMainList(width: number): string[] {
91
+ const lines: string[] = [];
92
+
93
+ if (this.searchEnabled && this.searchInput) {
94
+ lines.push(...this.searchInput.render(width));
95
+ lines.push("");
96
+ }
97
+
98
+ if (this.items.length === 0) {
99
+ lines.push(this.theme.hint(" No settings available"));
100
+ if (this.searchEnabled) {
101
+ this.addHintLine(lines, width);
102
+ }
103
+ return lines;
104
+ }
105
+
106
+ const displayItems = this.searchEnabled ? this.filteredItems : this.items;
107
+ if (displayItems.length === 0) {
108
+ lines.push(truncateToWidth(this.theme.hint(" No matching settings"), width));
109
+ this.addHintLine(lines, width);
110
+ return lines;
111
+ }
112
+
113
+ // Calculate visible range with scrolling
114
+ const startIndex = Math.max(
115
+ 0,
116
+ Math.min(
117
+ this.selectedIndex - Math.floor(this.maxVisible / 2),
118
+ displayItems.length - this.maxVisible
119
+ )
120
+ );
121
+ const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
122
+
123
+ // Calculate max label width for alignment
124
+ const maxLabelWidth = Math.min(
125
+ 30,
126
+ Math.max(...this.items.map((item) => visibleWidth(item.label)))
127
+ );
128
+
129
+ // Render visible items
130
+ for (let i = startIndex; i < endIndex; i++) {
131
+ const item = displayItems[i];
132
+ if (!item) continue;
133
+
134
+ const isSelected = i === this.selectedIndex;
135
+ const prefix = isSelected ? this.theme.cursor : " ";
136
+ const prefixWidth = visibleWidth(prefix);
137
+
138
+ // Pad label to align values
139
+ const labelPadded =
140
+ item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
141
+ const labelText = this.theme.label(labelPadded, isSelected);
142
+
143
+ // Calculate space for value
144
+ const separator = " ";
145
+ const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);
146
+ const valueMaxWidth = width - usedWidth - 2;
147
+
148
+ const valueText = this.theme.value(
149
+ truncateToWidth(item.currentValue, valueMaxWidth, ""),
150
+ isSelected
151
+ );
152
+
153
+ lines.push(truncateToWidth(prefix + labelText + separator + valueText, width));
154
+ }
155
+
156
+ // Add scroll indicator if needed
157
+ if (startIndex > 0 || endIndex < displayItems.length) {
158
+ const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
159
+ lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
160
+ }
161
+
162
+ // Add description for selected item
163
+ const selectedItem = displayItems[this.selectedIndex];
164
+ if (selectedItem?.description) {
165
+ lines.push("");
166
+ const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4);
167
+ for (const line of wrappedDesc) {
168
+ lines.push(this.theme.description(` ${line}`));
169
+ }
170
+ }
171
+
172
+ // Add hint
173
+ this.addHintLine(lines, width);
174
+
175
+ return lines;
176
+ }
177
+
178
+ handleInput(data: string): void {
179
+ // If submenu is active, delegate all input to it
180
+ // The submenu's onCancel (triggered by escape) will call done() which closes it
181
+ if (this.submenuComponent) {
182
+ this.submenuComponent.handleInput?.(data);
183
+ return;
184
+ }
185
+
186
+ // Main list input handling
187
+ const kb = getEditorKeybindings();
188
+ const displayItems = this.searchEnabled ? this.filteredItems : this.items;
189
+ if (kb.matches(data, "selectUp")) {
190
+ if (displayItems.length === 0) return;
191
+ this.selectedIndex =
192
+ this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
193
+ } else if (kb.matches(data, "selectDown")) {
194
+ if (displayItems.length === 0) return;
195
+ this.selectedIndex =
196
+ this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
197
+ } else if (kb.matches(data, "selectConfirm") || data === " ") {
198
+ this.activateItem();
199
+ } else if (kb.matches(data, "selectCancel")) {
200
+ this.onCancel();
201
+ } else if (this.searchEnabled && this.searchInput) {
202
+ const sanitized = data.replace(/ /g, "");
203
+ if (!sanitized) {
204
+ return;
205
+ }
206
+ this.searchInput.handleInput(sanitized);
207
+ this.applyFilter(this.searchInput.getValue());
208
+ }
209
+ }
210
+
211
+ private activateItem(): void {
212
+ const item = this.searchEnabled
213
+ ? this.filteredItems[this.selectedIndex]
214
+ : this.items[this.selectedIndex];
215
+ if (!item) return;
216
+
217
+ if (item.submenu) {
218
+ // Open submenu, passing current value so it can pre-select correctly
219
+ this.submenuItemIndex = this.selectedIndex;
220
+ this.submenuComponent = item.submenu(item.currentValue, (selectedValue?: string) => {
221
+ if (selectedValue !== undefined) {
222
+ item.currentValue = selectedValue;
223
+ this.onChange(item.id, selectedValue);
224
+ }
225
+ this.closeSubmenu();
226
+ });
227
+ } else if (item.values && item.values.length > 0) {
228
+ // Cycle through values
229
+ const currentIndex = item.values.indexOf(item.currentValue);
230
+ const nextIndex = (currentIndex + 1) % item.values.length;
231
+ const newValue = item.values[nextIndex];
232
+ item.currentValue = newValue;
233
+ this.onChange(item.id, newValue);
234
+ }
235
+ }
236
+
237
+ private closeSubmenu(): void {
238
+ this.submenuComponent = null;
239
+ // Restore selection to the item that opened the submenu
240
+ if (this.submenuItemIndex !== null) {
241
+ this.selectedIndex = this.submenuItemIndex;
242
+ this.submenuItemIndex = null;
243
+ }
244
+ }
245
+
246
+ private applyFilter(query: string): void {
247
+ this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
248
+ this.selectedIndex = 0;
249
+ }
250
+
251
+ private addHintLine(lines: string[], width: number): void {
252
+ lines.push("");
253
+ lines.push(
254
+ truncateToWidth(
255
+ this.theme.hint(
256
+ this.searchEnabled
257
+ ? " Type to search · Enter/Space to change · Esc to cancel"
258
+ : " Enter/Space to change · Esc to cancel"
259
+ ),
260
+ width
261
+ )
262
+ );
263
+ }
264
+ }
@@ -0,0 +1,28 @@
1
+ import type { Component } from "../tui.js";
2
+
3
+ /**
4
+ * Spacer component that renders empty lines
5
+ */
6
+ export class Spacer implements Component {
7
+ private lines: number;
8
+
9
+ constructor(lines: number = 1) {
10
+ this.lines = lines;
11
+ }
12
+
13
+ setLines(lines: number): void {
14
+ this.lines = lines;
15
+ }
16
+
17
+ invalidate(): void {
18
+ // No cached state to invalidate currently
19
+ }
20
+
21
+ render(_width: number): string[] {
22
+ const result: string[] = [];
23
+ for (let i = 0; i < this.lines; i++) {
24
+ result.push("");
25
+ }
26
+ return result;
27
+ }
28
+ }
@@ -0,0 +1,113 @@
1
+ import type { Component } from "../tui.js";
2
+ import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
3
+
4
+ /**
5
+ * Text component - displays multi-line text with word wrapping
6
+ */
7
+ export class Text implements Component {
8
+ private text: string;
9
+ private paddingX: number; // Left/right padding
10
+ private paddingY: number; // Top/bottom padding
11
+ private customBgFn?: (text: string) => string;
12
+
13
+ // Cache for rendered output
14
+ private cachedText?: string;
15
+ private cachedWidth?: number;
16
+ private cachedLines?: string[];
17
+
18
+ constructor(
19
+ text: string = "",
20
+ paddingX: number = 1,
21
+ paddingY: number = 1,
22
+ customBgFn?: (text: string) => string
23
+ ) {
24
+ this.text = text;
25
+ this.paddingX = paddingX;
26
+ this.paddingY = paddingY;
27
+ this.customBgFn = customBgFn;
28
+ }
29
+
30
+ setText(text: string): void {
31
+ this.text = text;
32
+ this.cachedText = undefined;
33
+ this.cachedWidth = undefined;
34
+ this.cachedLines = undefined;
35
+ }
36
+
37
+ setCustomBgFn(customBgFn?: (text: string) => string): void {
38
+ this.customBgFn = customBgFn;
39
+ this.cachedText = undefined;
40
+ this.cachedWidth = undefined;
41
+ this.cachedLines = undefined;
42
+ }
43
+
44
+ invalidate(): void {
45
+ this.cachedText = undefined;
46
+ this.cachedWidth = undefined;
47
+ this.cachedLines = undefined;
48
+ }
49
+
50
+ render(width: number): string[] {
51
+ // Check cache
52
+ if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
53
+ return this.cachedLines;
54
+ }
55
+
56
+ // Don't render anything if there's no actual text
57
+ if (!this.text || this.text.trim() === "") {
58
+ const result: string[] = [];
59
+ this.cachedText = this.text;
60
+ this.cachedWidth = width;
61
+ this.cachedLines = result;
62
+ return result;
63
+ }
64
+
65
+ // Replace tabs with 3 spaces
66
+ const normalizedText = this.text.replace(/\t/g, " ");
67
+
68
+ // Calculate content width (subtract left/right margins)
69
+ const contentWidth = Math.max(1, width - this.paddingX * 2);
70
+
71
+ // Wrap text (this preserves ANSI codes but does NOT pad)
72
+ const wrappedLines = wrapTextWithAnsi(normalizedText, contentWidth);
73
+
74
+ // Add margins and background to each line
75
+ const leftMargin = " ".repeat(this.paddingX);
76
+ const rightMargin = " ".repeat(this.paddingX);
77
+ const contentLines: string[] = [];
78
+
79
+ for (const line of wrappedLines) {
80
+ // Add margins
81
+ const lineWithMargins = leftMargin + line + rightMargin;
82
+
83
+ // Apply background if specified (this also pads to full width)
84
+ if (this.customBgFn) {
85
+ contentLines.push(applyBackgroundToLine(lineWithMargins, width, this.customBgFn));
86
+ } else {
87
+ // No background - just pad to width with spaces
88
+ const visibleLen = visibleWidth(lineWithMargins);
89
+ const paddingNeeded = Math.max(0, width - visibleLen);
90
+ contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
91
+ }
92
+ }
93
+
94
+ // Add top/bottom padding (empty lines)
95
+ const emptyLine = " ".repeat(width);
96
+ const emptyLines: string[] = [];
97
+ for (let i = 0; i < this.paddingY; i++) {
98
+ const line = this.customBgFn
99
+ ? applyBackgroundToLine(emptyLine, width, this.customBgFn)
100
+ : emptyLine;
101
+ emptyLines.push(line);
102
+ }
103
+
104
+ const result = [...emptyLines, ...contentLines, ...emptyLines];
105
+
106
+ // Update cache
107
+ this.cachedText = this.text;
108
+ this.cachedWidth = width;
109
+ this.cachedLines = result;
110
+
111
+ return result.length > 0 ? result : [""];
112
+ }
113
+ }
@@ -0,0 +1,65 @@
1
+ import type { Component } from "../tui.js";
2
+ import { truncateToWidth, visibleWidth } from "../utils.js";
3
+
4
+ /**
5
+ * Text component that truncates to fit viewport width
6
+ */
7
+ export class TruncatedText implements Component {
8
+ private text: string;
9
+ private paddingX: number;
10
+ private paddingY: number;
11
+
12
+ constructor(text: string, paddingX: number = 0, paddingY: number = 0) {
13
+ this.text = text;
14
+ this.paddingX = paddingX;
15
+ this.paddingY = paddingY;
16
+ }
17
+
18
+ invalidate(): void {
19
+ // No cached state to invalidate currently
20
+ }
21
+
22
+ render(width: number): string[] {
23
+ const result: string[] = [];
24
+
25
+ // Empty line padded to width
26
+ const emptyLine = " ".repeat(width);
27
+
28
+ // Add vertical padding above
29
+ for (let i = 0; i < this.paddingY; i++) {
30
+ result.push(emptyLine);
31
+ }
32
+
33
+ // Calculate available width after horizontal padding
34
+ const availableWidth = Math.max(1, width - this.paddingX * 2);
35
+
36
+ // Take only the first line (stop at newline)
37
+ let singleLineText = this.text;
38
+ const newlineIndex = this.text.indexOf("\n");
39
+ if (newlineIndex !== -1) {
40
+ singleLineText = this.text.substring(0, newlineIndex);
41
+ }
42
+
43
+ // Truncate text if needed (accounting for ANSI codes)
44
+ const displayText = truncateToWidth(singleLineText, availableWidth);
45
+
46
+ // Add horizontal padding
47
+ const leftPadding = " ".repeat(this.paddingX);
48
+ const rightPadding = " ".repeat(this.paddingX);
49
+ const lineWithPadding = leftPadding + displayText + rightPadding;
50
+
51
+ // Pad line to exactly width characters
52
+ const lineVisibleWidth = visibleWidth(lineWithPadding);
53
+ const paddingNeeded = Math.max(0, width - lineVisibleWidth);
54
+ const finalLine = lineWithPadding + " ".repeat(paddingNeeded);
55
+
56
+ result.push(finalLine);
57
+
58
+ // Add vertical padding below
59
+ for (let i = 0; i < this.paddingY; i++) {
60
+ result.push(emptyLine);
61
+ }
62
+
63
+ return result;
64
+ }
65
+ }