@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.
- package/dist/cli.js +7 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +24 -10
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +285 -148
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/interactive-reset.d.ts +49 -0
- package/dist/interactive-reset.d.ts.map +1 -0
- package/dist/interactive-reset.js +40 -0
- package/dist/interactive-reset.js.map +1 -0
- package/dist/pi-tui-editor-patch.d.ts +10 -0
- package/dist/pi-tui-editor-patch.d.ts.map +1 -0
- package/dist/pi-tui-editor-patch.js +159 -0
- package/dist/pi-tui-editor-patch.js.map +1 -0
- package/dist/pi-tui-patch.d.ts +2 -0
- package/dist/pi-tui-patch.d.ts.map +1 -0
- package/dist/pi-tui-patch.js +563 -0
- package/dist/pi-tui-patch.js.map +1 -0
- package/dist/pi-tui-settings-list-patch.d.ts +11 -0
- package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
- package/dist/pi-tui-settings-list-patch.js +38 -0
- package/dist/pi-tui-settings-list-patch.js.map +1 -0
- package/dist/reset-diagnostics.d.ts +69 -0
- package/dist/reset-diagnostics.d.ts.map +1 -0
- package/dist/reset-diagnostics.js +41 -0
- package/dist/reset-diagnostics.js.map +1 -0
- package/dist/sdk.d.ts +5 -21
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +180 -149
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts +1 -0
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +7 -17
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +6 -16
- package/extensions/__integration__/teams-runtime.test.ts +4 -1
- package/extensions/_icons/index.ts +2 -4
- package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
- package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
- package/extensions/_shared/image-metadata.ts +99 -0
- package/extensions/_shared/inline-preview.ts +1 -1
- package/extensions/_shared/pid-registry.ts +0 -1
- package/extensions/_shared/terminal-links.ts +22 -0
- package/extensions/ask-user-question-tool/index.ts +0 -3
- package/extensions/clear/__tests__/clear.test.ts +270 -3
- package/extensions/command-expansion/index.ts +1 -1
- package/extensions/context-files/index.ts +5 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
- package/extensions/context-fork/extension.json +1 -1
- package/extensions/context-fork/index.ts +32 -0
- package/extensions/edit-tool-enhanced/index.ts +2 -1
- package/extensions/hooks/index.ts +33 -11
- package/extensions/loop/index.ts +14 -1
- package/extensions/lsp/index.ts +64 -13
- package/extensions/lsp/package.json +2 -2
- package/extensions/permissions/__tests__/permissions.test.ts +4 -4
- package/extensions/random-spinner/index.ts +7 -642
- package/extensions/read-tool-enhanced/index.ts +6 -8
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +4 -5
- package/extensions/render-stabilizer/index.ts +6 -6
- package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +1 -1
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
- package/extensions/slash-command-bridge/index.ts +14 -2
- package/extensions/subagent-tool/index.ts +1 -1
- package/extensions/subagent-tool/model-resolver.ts +274 -7
- package/extensions/tasks/__tests__/state-ui.test.ts +3 -3
- package/extensions/tasks/__tests__/widget-subagents.test.ts +2 -2
- package/extensions/tasks/commands/register-tasks-extension.ts +10 -10
- package/extensions/tasks/state/index.ts +1 -1
- package/extensions/tasks/ui/index.ts +2 -7
- package/extensions/teams-tool/tools/register-extension.ts +1 -3
- package/extensions/web-search-tool/index.ts +2 -1
- package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +21 -6
- package/extensions/write-tool-enhanced/index.ts +2 -1
- package/node_modules/@mariozechner/pi-tui/README.md +56 -34
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +0 -2
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +12 -23
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
- package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +6 -6
- package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
- package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
- package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
- package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
- package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
- package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
- package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
- package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
- package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
- package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
- package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
- package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +11 -23
- package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
- package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
- package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
- package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
- package/package.json +13 -13
- package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
- package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
- package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
- package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
- package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
- package/packages/tallow-tui/node_modules/marked/README.md +5 -4
- package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
- package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
- package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
- package/packages/tallow-tui/node_modules/marked/package.json +26 -34
- package/runtime/model-metadata-overrides.ts +10 -1
- package/runtime/pid-schema.ts +26 -6
- package/skills/tallow-expert/SKILL.md +1 -3
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
- package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -49
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
- package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
- package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
- package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
- package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
158
|
+
return await new Promise((resolve) => {
|
|
159
|
+
if (signal.aborted) {
|
|
160
|
+
resolve([]);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
127
163
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
237
|
-
if (textBeforeCursor.startsWith("/")) {
|
|
310
|
+
if (!options.force && textBeforeCursor.startsWith("/")) {
|
|
238
311
|
const spaceIndex = textBeforeCursor.indexOf(" ");
|
|
239
312
|
|
|
240
313
|
if (spaceIndex === -1) {
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
287
|
-
|
|
341
|
+
const commandName = textBeforeCursor.slice(1, spaceIndex);
|
|
342
|
+
const argumentText = textBeforeCursor.slice(spaceIndex + 1);
|
|
288
343
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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:
|
|
311
|
-
prefix:
|
|
358
|
+
items: argumentSuggestions,
|
|
359
|
+
prefix: argumentText,
|
|
312
360
|
};
|
|
313
361
|
}
|
|
314
362
|
|
|
315
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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 {
|
|
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 =
|
|
31
|
-
if (kb.matches(data, "
|
|
30
|
+
const kb = getKeybindings();
|
|
31
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
32
32
|
this.abortController.abort();
|
|
33
33
|
this.onAbort?.();
|
|
34
34
|
}
|