@dungle-scrubs/tallow 0.9.3 → 0.9.6

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 (207) hide show
  1. package/dist/cli.js +7 -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 +24 -10
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +285 -148
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/interactive-reset.d.ts +49 -0
  10. package/dist/interactive-reset.d.ts.map +1 -0
  11. package/dist/interactive-reset.js +40 -0
  12. package/dist/interactive-reset.js.map +1 -0
  13. package/dist/pi-tui-editor-patch.d.ts +10 -0
  14. package/dist/pi-tui-editor-patch.d.ts.map +1 -0
  15. package/dist/pi-tui-editor-patch.js +159 -0
  16. package/dist/pi-tui-editor-patch.js.map +1 -0
  17. package/dist/pi-tui-patch.d.ts +2 -0
  18. package/dist/pi-tui-patch.d.ts.map +1 -0
  19. package/dist/pi-tui-patch.js +563 -0
  20. package/dist/pi-tui-patch.js.map +1 -0
  21. package/dist/pi-tui-settings-list-patch.d.ts +11 -0
  22. package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
  23. package/dist/pi-tui-settings-list-patch.js +38 -0
  24. package/dist/pi-tui-settings-list-patch.js.map +1 -0
  25. package/dist/reset-diagnostics.d.ts +69 -0
  26. package/dist/reset-diagnostics.d.ts.map +1 -0
  27. package/dist/reset-diagnostics.js +41 -0
  28. package/dist/reset-diagnostics.js.map +1 -0
  29. package/dist/sdk.d.ts +5 -21
  30. package/dist/sdk.d.ts.map +1 -1
  31. package/dist/sdk.js +180 -149
  32. package/dist/sdk.js.map +1 -1
  33. package/dist/workspace-transition-interactive.d.ts +1 -0
  34. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  35. package/dist/workspace-transition-interactive.js +7 -17
  36. package/dist/workspace-transition-interactive.js.map +1 -1
  37. package/extensions/__integration__/audit-findings.test.ts +6 -16
  38. package/extensions/__integration__/teams-runtime.test.ts +4 -1
  39. package/extensions/_icons/index.ts +2 -4
  40. package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
  41. package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
  42. package/extensions/_shared/image-metadata.ts +99 -0
  43. package/extensions/_shared/inline-preview.ts +1 -1
  44. package/extensions/_shared/pid-registry.ts +0 -1
  45. package/extensions/_shared/terminal-links.ts +22 -0
  46. package/extensions/ask-user-question-tool/index.ts +0 -3
  47. package/extensions/clear/__tests__/clear.test.ts +270 -3
  48. package/extensions/command-expansion/index.ts +1 -1
  49. package/extensions/context-files/index.ts +5 -1
  50. package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
  51. package/extensions/context-fork/extension.json +1 -1
  52. package/extensions/context-fork/index.ts +32 -0
  53. package/extensions/edit-tool-enhanced/index.ts +2 -1
  54. package/extensions/hooks/index.ts +33 -11
  55. package/extensions/loop/index.ts +14 -1
  56. package/extensions/lsp/index.ts +64 -13
  57. package/extensions/lsp/package.json +2 -2
  58. package/extensions/permissions/__tests__/permissions.test.ts +4 -4
  59. package/extensions/random-spinner/index.ts +7 -642
  60. package/extensions/read-tool-enhanced/index.ts +6 -8
  61. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +4 -5
  62. package/extensions/render-stabilizer/index.ts +6 -6
  63. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +1 -1
  64. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
  65. package/extensions/slash-command-bridge/index.ts +14 -2
  66. package/extensions/subagent-tool/index.ts +1 -1
  67. package/extensions/subagent-tool/model-resolver.ts +274 -7
  68. package/extensions/tasks/__tests__/state-ui.test.ts +3 -3
  69. package/extensions/tasks/__tests__/widget-subagents.test.ts +2 -2
  70. package/extensions/tasks/commands/register-tasks-extension.ts +10 -10
  71. package/extensions/tasks/state/index.ts +1 -1
  72. package/extensions/tasks/ui/index.ts +2 -7
  73. package/extensions/teams-tool/tools/register-extension.ts +1 -3
  74. package/extensions/web-search-tool/index.ts +2 -1
  75. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +21 -6
  76. package/extensions/write-tool-enhanced/index.ts +2 -1
  77. package/node_modules/@mariozechner/pi-tui/README.md +56 -34
  78. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
  79. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
  80. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
  81. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
  83. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
  84. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
  85. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
  86. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
  87. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
  88. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
  89. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
  91. package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
  93. package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
  94. package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
  95. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
  96. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
  97. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
  98. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
  101. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
  102. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
  103. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
  104. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
  105. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
  106. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +0 -2
  107. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
  108. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +12 -23
  109. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
  110. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
  111. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  112. package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
  113. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  114. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
  115. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  116. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
  117. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  118. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
  119. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  120. package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
  121. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  122. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
  123. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
  124. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
  125. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
  126. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
  127. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  128. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
  129. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  130. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
  131. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  132. package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
  133. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  134. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
  135. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
  136. package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
  137. package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
  138. package/node_modules/@mariozechner/pi-tui/package.json +6 -6
  139. package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
  140. package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
  141. package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
  142. package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
  143. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
  144. package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
  145. package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
  146. package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
  147. package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
  148. package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
  149. package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
  150. package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
  151. package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
  152. package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
  153. package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +11 -23
  154. package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
  155. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
  156. package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
  157. package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
  158. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
  159. package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
  160. package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
  161. package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
  162. package/package.json +13 -13
  163. package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
  164. package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
  165. package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
  166. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
  167. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
  168. package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
  169. package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
  170. package/packages/tallow-tui/node_modules/marked/README.md +5 -4
  171. package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
  172. package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
  173. package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
  174. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
  175. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
  176. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
  177. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
  178. package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
  179. package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
  180. package/packages/tallow-tui/node_modules/marked/package.json +26 -34
  181. package/runtime/model-metadata-overrides.ts +10 -1
  182. package/runtime/pid-schema.ts +26 -6
  183. package/skills/tallow-expert/SKILL.md +1 -3
  184. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
  185. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
  186. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
  187. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
  188. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
  189. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
  190. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
  191. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
  192. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
  193. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
  194. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
  195. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
  196. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
  197. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
  198. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
  199. package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -49
  200. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
  201. package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
  202. package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
  203. package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
  204. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
  205. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
  206. package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
  207. package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { readdirSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, join } from "node:path";
@@ -6,6 +6,42 @@ import { fuzzyFilter } from "./fuzzy.js";
6
6
 
7
7
  const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
8
8
 
9
+ function toDisplayPath(value: string): string {
10
+ return value.replace(/\\/g, "/");
11
+ }
12
+
13
+ function escapeRegex(value: string): string {
14
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
+ }
16
+
17
+ function buildFdPathQuery(query: string): string {
18
+ const normalized = toDisplayPath(query);
19
+ if (!normalized.includes("/")) {
20
+ return normalized;
21
+ }
22
+
23
+ const hasTrailingSeparator = normalized.endsWith("/");
24
+ const trimmed = normalized.replace(/^\/+|\/+$/g, "");
25
+ if (!trimmed) {
26
+ return normalized;
27
+ }
28
+
29
+ const separatorPattern = "[\\\\/]";
30
+ const segments = trimmed
31
+ .split("/")
32
+ .filter(Boolean)
33
+ .map((segment) => escapeRegex(segment));
34
+ if (segments.length === 0) {
35
+ return normalized;
36
+ }
37
+
38
+ let pattern = segments.join(separatorPattern);
39
+ if (hasTrailingSeparator) {
40
+ pattern += separatorPattern;
41
+ }
42
+ return pattern;
43
+ }
44
+
9
45
  function findLastDelimiter(text: string): number {
10
46
  for (let i = text.length - 1; i >= 0; i -= 1) {
11
47
  if (PATH_DELIMITERS.has(text[i] ?? "")) {
@@ -89,12 +125,13 @@ function buildCompletionValue(
89
125
  }
90
126
 
91
127
  // Use fd to walk directory tree (fast, respects .gitignore)
92
- function walkDirectoryWithFd(
128
+ async function walkDirectoryWithFd(
93
129
  baseDir: string,
94
130
  fdPath: string,
95
131
  query: string,
96
- maxResults: number
97
- ): Array<{ path: string; isDirectory: boolean }> {
132
+ maxResults: number,
133
+ signal: AbortSignal
134
+ ): Promise<Array<{ path: string; isDirectory: boolean }>> {
98
135
  const args = [
99
136
  "--base-directory",
100
137
  baseDir,
@@ -114,43 +151,73 @@ function walkDirectoryWithFd(
114
151
  ".git/**",
115
152
  ];
116
153
 
117
- // Add query as pattern if provided
118
154
  if (query) {
119
- args.push(query);
155
+ args.push(buildFdPathQuery(query));
120
156
  }
121
157
 
122
- const result = spawnSync(fdPath, args, {
123
- encoding: "utf-8",
124
- stdio: ["pipe", "pipe", "pipe"],
125
- maxBuffer: 10 * 1024 * 1024,
126
- });
158
+ return await new Promise((resolve) => {
159
+ if (signal.aborted) {
160
+ resolve([]);
161
+ return;
162
+ }
127
163
 
128
- if (result.status !== 0 || !result.stdout) {
129
- return [];
130
- }
164
+ const child = spawn(fdPath, args, {
165
+ stdio: ["ignore", "pipe", "pipe"],
166
+ });
167
+ let stdout = "";
168
+ let resolved = false;
169
+
170
+ const finish = (results: Array<{ path: string; isDirectory: boolean }>) => {
171
+ if (resolved) return;
172
+ resolved = true;
173
+ signal.removeEventListener("abort", onAbort);
174
+ resolve(results);
175
+ };
131
176
 
132
- const lines = result.stdout.trim().split("\n").filter(Boolean);
133
- const results: Array<{ path: string; isDirectory: boolean }> = [];
134
-
135
- for (const line of lines) {
136
- const normalizedPath = line.endsWith("/") ? line.slice(0, -1) : line;
137
- if (
138
- normalizedPath === ".git" ||
139
- normalizedPath.startsWith(".git/") ||
140
- normalizedPath.includes("/.git/")
141
- ) {
142
- continue;
143
- }
177
+ const onAbort = () => {
178
+ if (child.exitCode === null) {
179
+ child.kill("SIGKILL");
180
+ }
181
+ };
144
182
 
145
- // fd outputs directories with trailing /
146
- const isDirectory = line.endsWith("/");
147
- results.push({
148
- path: line,
149
- isDirectory,
183
+ signal.addEventListener("abort", onAbort, { once: true });
184
+ child.stdout.setEncoding("utf-8");
185
+ child.stdout.on("data", (chunk: string) => {
186
+ stdout += chunk;
150
187
  });
151
- }
188
+ child.on("error", () => {
189
+ finish([]);
190
+ });
191
+ child.on("close", (code) => {
192
+ if (signal.aborted || code !== 0 || !stdout) {
193
+ finish([]);
194
+ return;
195
+ }
196
+
197
+ const lines = stdout.trim().split("\n").filter(Boolean);
198
+ const results: Array<{ path: string; isDirectory: boolean }> = [];
199
+
200
+ for (const line of lines) {
201
+ const displayLine = toDisplayPath(line);
202
+ const hasTrailingSeparator = displayLine.endsWith("/");
203
+ const normalizedPath = hasTrailingSeparator ? displayLine.slice(0, -1) : displayLine;
204
+ if (
205
+ normalizedPath === ".git" ||
206
+ normalizedPath.startsWith(".git/") ||
207
+ normalizedPath.includes("/.git/")
208
+ ) {
209
+ continue;
210
+ }
152
211
 
153
- return results;
212
+ results.push({
213
+ path: displayLine,
214
+ isDirectory: hasTrailingSeparator,
215
+ });
216
+ }
217
+
218
+ finish(results);
219
+ });
220
+ });
154
221
  }
155
222
 
156
223
  export interface AutocompleteItem {
@@ -159,12 +226,20 @@ export interface AutocompleteItem {
159
226
  description?: string;
160
227
  }
161
228
 
229
+ type Awaitable<T> = T | Promise<T>;
230
+
162
231
  export interface SlashCommand {
163
232
  name: string;
164
233
  description?: string;
234
+ argumentHint?: string;
165
235
  // Function to get argument completions for this command
166
236
  // Returns null if no argument completion is available
167
- getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
237
+ getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
238
+ }
239
+
240
+ export interface AutocompleteSuggestions {
241
+ items: AutocompleteItem[];
242
+ prefix: string; // What we're matching against (e.g., "/" or "src/")
168
243
  }
169
244
 
170
245
  export interface AutocompleteProvider {
@@ -173,11 +248,9 @@ export interface AutocompleteProvider {
173
248
  getSuggestions(
174
249
  lines: string[],
175
250
  cursorLine: number,
176
- cursorCol: number
177
- ): {
178
- items: AutocompleteItem[];
179
- prefix: string; // What we're matching against (e.g., "/" or "src/")
180
- } | null;
251
+ cursorCol: number,
252
+ options: { signal: AbortSignal; force?: boolean }
253
+ ): Promise<AutocompleteSuggestions | null>;
181
254
 
182
255
  // Apply the selected item
183
256
  // Returns the new text and cursor position
@@ -210,20 +283,21 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
210
283
  this.fdPath = fdPath;
211
284
  }
212
285
 
213
- getSuggestions(
286
+ async getSuggestions(
214
287
  lines: string[],
215
288
  cursorLine: number,
216
- cursorCol: number
217
- ): { items: AutocompleteItem[]; prefix: string } | null {
289
+ cursorCol: number,
290
+ options: { signal: AbortSignal; force?: boolean }
291
+ ): Promise<AutocompleteSuggestions | null> {
218
292
  const currentLine = lines[cursorLine] || "";
219
293
  const textBeforeCursor = currentLine.slice(0, cursorCol);
220
294
 
221
- // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
222
295
  const atPrefix = this.extractAtPrefix(textBeforeCursor);
223
296
  if (atPrefix) {
224
297
  const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
225
- const suggestions = this.getFuzzyFileSuggestions(rawPrefix, {
226
- isQuotedPrefix: isQuotedPrefix,
298
+ const suggestions = await this.getFuzzyFileSuggestions(rawPrefix, {
299
+ isQuotedPrefix,
300
+ signal: options.signal,
227
301
  });
228
302
  if (suggestions.length === 0) return null;
229
303
 
@@ -233,18 +307,22 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
233
307
  };
234
308
  }
235
309
 
236
- // Check for slash commands
237
- if (textBeforeCursor.startsWith("/")) {
310
+ if (!options.force && textBeforeCursor.startsWith("/")) {
238
311
  const spaceIndex = textBeforeCursor.indexOf(" ");
239
312
 
240
313
  if (spaceIndex === -1) {
241
- // No space yet - complete command names with fuzzy matching
242
- const prefix = textBeforeCursor.slice(1); // Remove the "/"
243
- const commandItems = this.commands.map((cmd) => ({
244
- name: "name" in cmd ? cmd.name : cmd.value,
245
- label: "name" in cmd ? cmd.name : cmd.label,
246
- description: cmd.description,
247
- }));
314
+ const prefix = textBeforeCursor.slice(1);
315
+ const commandItems = this.commands.map((cmd) => {
316
+ const name = "name" in cmd ? cmd.name : cmd.value;
317
+ const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
318
+ const desc = cmd.description ?? "";
319
+ const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
320
+ return {
321
+ name,
322
+ label: name,
323
+ description: fullDesc || undefined,
324
+ };
325
+ });
248
326
 
249
327
  const filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({
250
328
  value: item.name,
@@ -258,61 +336,42 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
258
336
  items: filtered,
259
337
  prefix: textBeforeCursor,
260
338
  };
261
- } else {
262
- // Space found - complete command arguments
263
- const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
264
- const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
265
-
266
- const command = this.commands.find((cmd) => {
267
- const name = "name" in cmd ? cmd.name : cmd.value;
268
- return name === commandName;
269
- });
270
- if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
271
- return null; // No argument completion for this command
272
- }
273
-
274
- const argumentSuggestions = command.getArgumentCompletions(argumentText);
275
- if (!argumentSuggestions || argumentSuggestions.length === 0) {
276
- return null;
277
- }
278
-
279
- return {
280
- items: argumentSuggestions,
281
- prefix: argumentText,
282
- };
283
339
  }
284
- }
285
340
 
286
- // Check for file paths - triggered by Tab or if we detect a path pattern
287
- const pathMatch = this.extractPathPrefix(textBeforeCursor, false);
341
+ const commandName = textBeforeCursor.slice(1, spaceIndex);
342
+ const argumentText = textBeforeCursor.slice(spaceIndex + 1);
288
343
 
289
- if (pathMatch !== null) {
290
- const suggestions = this.getFileSuggestions(pathMatch);
291
- if (suggestions.length === 0) return null;
344
+ const command = this.commands.find((cmd) => {
345
+ const name = "name" in cmd ? cmd.name : cmd.value;
346
+ return name === commandName;
347
+ });
348
+ if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
349
+ return null;
350
+ }
292
351
 
293
- // Check if we have an exact match that is a directory
294
- // In that case, we might want to return suggestions for the directory content instead
295
- // But only if the prefix ends with /
296
- if (
297
- suggestions.length === 1 &&
298
- suggestions[0]?.value === pathMatch &&
299
- !pathMatch.endsWith("/")
300
- ) {
301
- // Exact match found (e.g. user typed "src" and "src/" is the only match)
302
- // We still return it so user can select it and add /
303
- return {
304
- items: suggestions,
305
- prefix: pathMatch,
306
- };
352
+ const argumentSuggestions = await command.getArgumentCompletions(argumentText);
353
+ if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
354
+ return null;
307
355
  }
308
356
 
309
357
  return {
310
- items: suggestions,
311
- prefix: pathMatch,
358
+ items: argumentSuggestions,
359
+ prefix: argumentText,
312
360
  };
313
361
  }
314
362
 
315
- return null;
363
+ const pathMatch = this.extractPathPrefix(textBeforeCursor, options.force ?? false);
364
+ if (pathMatch === null) {
365
+ return null;
366
+ }
367
+
368
+ const suggestions = this.getFileSuggestions(pathMatch);
369
+ if (suggestions.length === 0) return null;
370
+
371
+ return {
372
+ items: suggestions,
373
+ prefix: pathMatch,
374
+ };
316
375
  }
317
376
 
318
377
  applyCompletion(
@@ -467,6 +526,46 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
467
526
  return path;
468
527
  }
469
528
 
529
+ private resolveScopedFuzzyQuery(
530
+ rawQuery: string
531
+ ): { baseDir: string; query: string; displayBase: string } | null {
532
+ const normalizedQuery = toDisplayPath(rawQuery);
533
+ const slashIndex = normalizedQuery.lastIndexOf("/");
534
+ if (slashIndex === -1) {
535
+ return null;
536
+ }
537
+
538
+ const displayBase = normalizedQuery.slice(0, slashIndex + 1);
539
+ const query = normalizedQuery.slice(slashIndex + 1);
540
+
541
+ let baseDir: string;
542
+ if (displayBase.startsWith("~/")) {
543
+ baseDir = this.expandHomePath(displayBase);
544
+ } else if (displayBase.startsWith("/")) {
545
+ baseDir = displayBase;
546
+ } else {
547
+ baseDir = join(this.basePath, displayBase);
548
+ }
549
+
550
+ try {
551
+ if (!statSync(baseDir).isDirectory()) {
552
+ return null;
553
+ }
554
+ } catch {
555
+ return null;
556
+ }
557
+
558
+ return { baseDir, query, displayBase };
559
+ }
560
+
561
+ private scopedPathForDisplay(displayBase: string, relativePath: string): string {
562
+ const normalizedRelativePath = toDisplayPath(relativePath);
563
+ if (displayBase === "/") {
564
+ return `/${normalizedRelativePath}`;
565
+ }
566
+ return `${toDisplayPath(displayBase)}${normalizedRelativePath}`;
567
+ }
568
+
470
569
  // Get file/directory suggestions for a given path prefix
471
570
  private getFileSuggestions(prefix: string): AutocompleteItem[] {
472
571
  try {
@@ -543,7 +642,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
543
642
  if (displayPrefix.endsWith("/")) {
544
643
  // If prefix ends with /, append entry to the prefix
545
644
  relativePath = displayPrefix + name;
546
- } else if (displayPrefix.includes("/")) {
645
+ } else if (displayPrefix.includes("/") || displayPrefix.includes("\\")) {
547
646
  // Preserve ~/ format for home directory paths
548
647
  if (displayPrefix.startsWith("~/")) {
549
648
  const homeRelativeDir = displayPrefix.slice(2); // Remove ~/
@@ -559,6 +658,10 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
559
658
  }
560
659
  } else {
561
660
  relativePath = join(dirname(displayPrefix), name);
661
+ // path.join normalizes away ./ prefix, preserve it
662
+ if (displayPrefix.startsWith("./") && !relativePath.startsWith("./")) {
663
+ relativePath = `./${relativePath}`;
664
+ }
562
665
  }
563
666
  } else {
564
667
  // For standalone entries, preserve ~/ if original prefix was ~/
@@ -569,6 +672,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
569
672
  }
570
673
  }
571
674
 
675
+ relativePath = toDisplayPath(relativePath);
572
676
  const pathValue = isDirectory ? `${relativePath}/` : relativePath;
573
677
  const value = buildCompletionValue(pathValue, {
574
678
  isDirectory,
@@ -623,37 +727,48 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
623
727
  }
624
728
 
625
729
  // Fuzzy file search using fd (fast, respects .gitignore)
626
- private getFuzzyFileSuggestions(
730
+ private async getFuzzyFileSuggestions(
627
731
  query: string,
628
- options: { isQuotedPrefix: boolean }
629
- ): AutocompleteItem[] {
630
- if (!this.fdPath) {
631
- // fd not available, return empty results
732
+ options: { isQuotedPrefix: boolean; signal: AbortSignal }
733
+ ): Promise<AutocompleteItem[]> {
734
+ if (!this.fdPath || options.signal.aborted) {
632
735
  return [];
633
736
  }
634
737
 
635
738
  try {
636
- const entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);
739
+ const scopedQuery = this.resolveScopedFuzzyQuery(query);
740
+ const fdBaseDir = scopedQuery?.baseDir ?? this.basePath;
741
+ const fdQuery = scopedQuery?.query ?? query;
742
+ const entries = await walkDirectoryWithFd(
743
+ fdBaseDir,
744
+ this.fdPath,
745
+ fdQuery,
746
+ 100,
747
+ options.signal
748
+ );
749
+ if (options.signal.aborted) {
750
+ return [];
751
+ }
637
752
 
638
- // Score entries
639
753
  const scoredEntries = entries
640
754
  .map((entry) => ({
641
755
  ...entry,
642
- score: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,
756
+ score: fdQuery ? this.scoreEntry(entry.path, fdQuery, entry.isDirectory) : 1,
643
757
  }))
644
758
  .filter((entry) => entry.score > 0);
645
759
 
646
- // Sort by score (descending) and take top 20
647
760
  scoredEntries.sort((a, b) => b.score - a.score);
648
761
  const topEntries = scoredEntries.slice(0, 20);
649
762
 
650
- // Build suggestions
651
763
  const suggestions: AutocompleteItem[] = [];
652
764
  for (const { path: entryPath, isDirectory } of topEntries) {
653
- // fd already includes trailing / for directories
654
765
  const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;
766
+ const displayPath = scopedQuery
767
+ ? this.scopedPathForDisplay(scopedQuery.displayBase, pathWithoutSlash)
768
+ : pathWithoutSlash;
655
769
  const entryName = basename(pathWithoutSlash);
656
- const value = buildCompletionValue(entryPath, {
770
+ const completionPath = isDirectory ? `${displayPath}/` : displayPath;
771
+ const value = buildCompletionValue(completionPath, {
657
772
  isDirectory,
658
773
  isAtPrefix: true,
659
774
  isQuotedPrefix: options.isQuotedPrefix,
@@ -662,7 +777,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
662
777
  suggestions.push({
663
778
  value,
664
779
  label: entryName + (isDirectory ? "/" : ""),
665
- description: pathWithoutSlash,
780
+ description: displayPath,
666
781
  });
667
782
  }
668
783
 
@@ -672,35 +787,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
672
787
  }
673
788
  }
674
789
 
675
- // Force file completion (called on Tab key) - always returns suggestions
676
- getForceFileSuggestions(
677
- lines: string[],
678
- cursorLine: number,
679
- cursorCol: number
680
- ): { items: AutocompleteItem[]; prefix: string } | null {
681
- const currentLine = lines[cursorLine] || "";
682
- const textBeforeCursor = currentLine.slice(0, cursorCol);
683
-
684
- // Don't trigger if we're typing a slash command at the start of the line
685
- if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) {
686
- return null;
687
- }
688
-
689
- // Force extract path prefix - this will always return something
690
- const pathMatch = this.extractPathPrefix(textBeforeCursor, true);
691
- if (pathMatch !== null) {
692
- const suggestions = this.getFileSuggestions(pathMatch);
693
- if (suggestions.length === 0) return null;
694
-
695
- return {
696
- items: suggestions,
697
- prefix: pathMatch,
698
- };
699
- }
700
-
701
- return null;
702
- }
703
-
704
790
  // Check if we should trigger file completion (called on Tab key)
705
791
  shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {
706
792
  const currentLine = lines[cursorLine] || "";
@@ -1,4 +1,4 @@
1
- import { getEditorKeybindings } from "../keybindings.js";
1
+ import { getKeybindings } from "../keybindings.js";
2
2
  import { Loader } from "./loader.js";
3
3
 
4
4
  /**
@@ -27,8 +27,8 @@ export class CancellableLoader extends Loader {
27
27
  }
28
28
 
29
29
  handleInput(data: string): void {
30
- const kb = getEditorKeybindings();
31
- if (kb.matches(data, "selectCancel")) {
30
+ const kb = getKeybindings();
31
+ if (kb.matches(data, "tui.select.cancel")) {
32
32
  this.abortController.abort();
33
33
  this.onAbort?.();
34
34
  }