@ramarivera/pi-television 0.0.3 → 0.0.5
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/README.md +49 -3
- package/package.json +2 -2
- package/src/extension.ts +362 -108
- package/src/index.ts +11 -2
package/README.md
CHANGED
|
@@ -1,13 +1,60 @@
|
|
|
1
1
|
# @ramarivera/pi-television
|
|
2
2
|
|
|
3
|
-
Pi extension that
|
|
3
|
+
Pi extension that keeps Pi's native file picking UX while replacing the default `@file` search path with a faster background television-style search.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pi install npm:@ramarivera/pi-television@0.0.
|
|
8
|
+
pi install npm:@ramarivera/pi-television@0.0.4
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Modes
|
|
12
|
+
|
|
13
|
+
### Default: native live picker
|
|
14
|
+
|
|
15
|
+
By default, typing `@` in Pi keeps using Pi's native picker UI, but the suggestions come from this extension's background file search instead of launching the full-screen `tv` interface.
|
|
16
|
+
|
|
17
|
+
### Optional: select dialog mode
|
|
18
|
+
|
|
19
|
+
If you want the simpler fallback flow, create `.pi/television.json` in your project:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mode": "select-dialog"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That mode uses background search plus a native Pi select dialog when you trigger `@`.
|
|
28
|
+
|
|
29
|
+
## Config
|
|
30
|
+
|
|
31
|
+
Project config lives at:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
.pi/television.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
You can also set a user-level default at:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
~/.pi/agent/television.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Project config overrides user config.
|
|
44
|
+
|
|
45
|
+
Supported fields:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mode": "native-live",
|
|
50
|
+
"includeFolders": true,
|
|
51
|
+
"maxResults": 20,
|
|
52
|
+
"refreshMs": 10000
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`includeFolders` defaults to `true`, so folder paths are returned alongside files. Set it to `false` to restrict the picker to regular files only.
|
|
57
|
+
|
|
11
58
|
## Local Development
|
|
12
59
|
|
|
13
60
|
This checkout is live-enabled for Pi through:
|
|
@@ -37,4 +84,3 @@ Before the first publish, configure npm trusted publishing:
|
|
|
37
84
|
- environment: blank unless the workflow is changed to require one
|
|
38
85
|
|
|
39
86
|
No `NPM_TOKEN` is required for trusted publishing.
|
|
40
|
-
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramarivera/pi-television",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Pi extension that
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Pi extension that powers native Pi @file picking with background television-style search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Ramiro Rivera",
|
package/src/extension.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, relative, resolve } from "node:path";
|
|
3
4
|
import type {
|
|
4
5
|
ExtensionAPI,
|
|
5
6
|
ExtensionContext,
|
|
6
7
|
TerminalInputHandler,
|
|
7
8
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
9
|
+
import type {
|
|
10
|
+
AutocompleteItem,
|
|
11
|
+
AutocompleteProvider,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
import {
|
|
14
|
+
decodeKittyPrintable,
|
|
15
|
+
fuzzyFilter,
|
|
16
|
+
Key,
|
|
17
|
+
matchesKey,
|
|
18
|
+
} from "@earendil-works/pi-tui";
|
|
9
19
|
|
|
10
20
|
export type ExtensionInfo = {
|
|
11
21
|
name: string;
|
|
@@ -17,121 +27,306 @@ export type TelevisionPickResult =
|
|
|
17
27
|
| { status: "cancelled" }
|
|
18
28
|
| { status: "failed"; message: string };
|
|
19
29
|
|
|
20
|
-
export type
|
|
30
|
+
export type TelevisionMode = "native-live" | "select-dialog";
|
|
31
|
+
|
|
32
|
+
export type TelevisionConfig = {
|
|
33
|
+
mode?: TelevisionMode;
|
|
34
|
+
maxResults?: number;
|
|
35
|
+
refreshMs?: number;
|
|
36
|
+
includeFolders?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type TelevisionResolvedConfig = {
|
|
40
|
+
mode: TelevisionMode;
|
|
41
|
+
maxResults: number;
|
|
42
|
+
refreshMs: number;
|
|
43
|
+
includeFolders: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TelevisionSearchResult = {
|
|
47
|
+
path: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type TelevisionSearchOptions = {
|
|
21
53
|
cwd: string;
|
|
22
54
|
query?: string;
|
|
23
55
|
signal?: AbortSignal;
|
|
56
|
+
maxResults?: number;
|
|
57
|
+
refreshMs?: number;
|
|
58
|
+
includeFolders?: boolean;
|
|
24
59
|
};
|
|
25
60
|
|
|
26
|
-
export type
|
|
27
|
-
options:
|
|
28
|
-
) => Promise<
|
|
61
|
+
export type TelevisionSearcher = (
|
|
62
|
+
options: TelevisionSearchOptions,
|
|
63
|
+
) => Promise<TelevisionSearchResult[]>;
|
|
64
|
+
|
|
65
|
+
export type TelevisionConfigLoader = (
|
|
66
|
+
cwd: string,
|
|
67
|
+
) => Promise<TelevisionResolvedConfig>;
|
|
29
68
|
|
|
30
69
|
export type TelevisionExtensionOptions = {
|
|
31
70
|
commandName?: string;
|
|
32
71
|
shortcut?: string;
|
|
33
|
-
|
|
72
|
+
searcher?: TelevisionSearcher;
|
|
73
|
+
configLoader?: TelevisionConfigLoader;
|
|
34
74
|
};
|
|
35
75
|
|
|
36
76
|
const STATUS_KEY = "pi-television";
|
|
37
77
|
const DEFAULT_COMMAND_NAME = "television";
|
|
38
78
|
const DEFAULT_SHORTCUT = "@";
|
|
79
|
+
const DEFAULT_MAX_RESULTS = 20;
|
|
80
|
+
const DEFAULT_REFRESH_MS = 10_000;
|
|
39
81
|
|
|
40
82
|
export const extensionInfo: ExtensionInfo = {
|
|
41
83
|
name: "television",
|
|
42
84
|
description:
|
|
43
|
-
"Pi extension that
|
|
85
|
+
"Pi extension that powers native @file picking with background television-style search",
|
|
44
86
|
};
|
|
45
87
|
|
|
46
|
-
|
|
47
|
-
|
|
88
|
+
const DEFAULT_INCLUDE_FOLDERS = true;
|
|
89
|
+
|
|
90
|
+
const defaultResolvedConfig: TelevisionResolvedConfig = {
|
|
91
|
+
mode: "native-live",
|
|
92
|
+
maxResults: DEFAULT_MAX_RESULTS,
|
|
93
|
+
refreshMs: DEFAULT_REFRESH_MS,
|
|
94
|
+
includeFolders: DEFAULT_INCLUDE_FOLDERS,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type FileIndexCache = {
|
|
98
|
+
loadedAt: number;
|
|
99
|
+
entries?: string[];
|
|
100
|
+
pending?: Promise<string[]>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function uniq(values: string[]): string[] {
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
const deduped: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const value of values) {
|
|
108
|
+
if (seen.has(value)) continue;
|
|
109
|
+
seen.add(value);
|
|
110
|
+
deduped.push(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return deduped;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cleanPaths(stdout: string): string[] {
|
|
117
|
+
return stdout
|
|
48
118
|
.split(/\r?\n/)
|
|
49
119
|
.map((line) => line.trim())
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
120
|
+
.filter((line) => line.length > 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractFileToken(textBeforeCursor: string): string | undefined {
|
|
124
|
+
const match = textBeforeCursor.match(/(?:^|[ \t])@([^\s@]*)$/);
|
|
125
|
+
return match?.[1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toAutocompleteItem(result: TelevisionSearchResult): AutocompleteItem {
|
|
129
|
+
return {
|
|
130
|
+
value: `@${result.path}`,
|
|
131
|
+
label: result.label ?? result.path,
|
|
132
|
+
description: result.description ?? result.path,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeTelevisionConfig(
|
|
137
|
+
config: TelevisionConfig | undefined,
|
|
138
|
+
): TelevisionResolvedConfig {
|
|
139
|
+
return {
|
|
140
|
+
mode: config?.mode ?? defaultResolvedConfig.mode,
|
|
141
|
+
maxResults: config?.maxResults ?? defaultResolvedConfig.maxResults,
|
|
142
|
+
refreshMs: config?.refreshMs ?? defaultResolvedConfig.refreshMs,
|
|
143
|
+
includeFolders:
|
|
144
|
+
config?.includeFolders ?? defaultResolvedConfig.includeFolders,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadConfigFile(
|
|
149
|
+
path: string,
|
|
150
|
+
): Promise<TelevisionConfig | undefined> {
|
|
151
|
+
try {
|
|
152
|
+
const content = await readFile(path, "utf8");
|
|
153
|
+
const parsed = JSON.parse(content) as TelevisionConfig;
|
|
154
|
+
return parsed;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
157
|
+
if (nodeError.code === "ENOENT") {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`television: failed to read ${path}: ${nodeError.message}`);
|
|
72
161
|
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function loadTelevisionConfig(
|
|
165
|
+
cwd: string,
|
|
166
|
+
): Promise<TelevisionResolvedConfig> {
|
|
167
|
+
const globalConfig = await loadConfigFile(
|
|
168
|
+
resolve(homedir(), ".pi", "agent", "television.json"),
|
|
169
|
+
);
|
|
170
|
+
const projectConfig = await loadConfigFile(
|
|
171
|
+
resolve(cwd, ".pi", "television.json"),
|
|
172
|
+
);
|
|
73
173
|
|
|
74
|
-
return
|
|
174
|
+
return normalizeTelevisionConfig({
|
|
175
|
+
...globalConfig,
|
|
176
|
+
...projectConfig,
|
|
177
|
+
});
|
|
75
178
|
}
|
|
76
179
|
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
180
|
+
export function rankTelevisionResults(
|
|
181
|
+
paths: string[],
|
|
182
|
+
query: string | undefined,
|
|
183
|
+
maxResults: number,
|
|
184
|
+
): TelevisionSearchResult[] {
|
|
185
|
+
const trimmedQuery = query?.trim() ?? "";
|
|
186
|
+
const limitedMaxResults = Math.max(1, maxResults);
|
|
187
|
+
|
|
188
|
+
if (!trimmedQuery) {
|
|
189
|
+
return paths.slice(0, limitedMaxResults).map((path) => ({ path }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const exactPrefixMatches = paths.filter((path) =>
|
|
193
|
+
path.startsWith(trimmedQuery),
|
|
194
|
+
);
|
|
195
|
+
const basenamePrefixMatches = paths.filter(
|
|
196
|
+
(path) =>
|
|
197
|
+
basename(path).startsWith(trimmedQuery) &&
|
|
198
|
+
!exactPrefixMatches.includes(path),
|
|
199
|
+
);
|
|
200
|
+
const fuzzyMatches = fuzzyFilter(paths, trimmedQuery, (path) => {
|
|
201
|
+
const name = basename(path);
|
|
202
|
+
return name === path ? path : `${name} ${path}`;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return uniq([
|
|
206
|
+
...exactPrefixMatches,
|
|
207
|
+
...basenamePrefixMatches,
|
|
208
|
+
...fuzzyMatches,
|
|
209
|
+
])
|
|
210
|
+
.slice(0, limitedMaxResults)
|
|
211
|
+
.map((path) => ({ path }));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createDefaultSearcher(pi: ExtensionAPI): TelevisionSearcher {
|
|
215
|
+
const cache = new Map<string, FileIndexCache>();
|
|
216
|
+
|
|
217
|
+
return async ({
|
|
218
|
+
cwd,
|
|
219
|
+
query,
|
|
220
|
+
signal,
|
|
221
|
+
maxResults = DEFAULT_MAX_RESULTS,
|
|
222
|
+
refreshMs = DEFAULT_REFRESH_MS,
|
|
223
|
+
includeFolders = DEFAULT_INCLUDE_FOLDERS,
|
|
224
|
+
}) => {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const cached = cache.get(cwd);
|
|
227
|
+
|
|
228
|
+
if (cached?.entries && now - cached.loadedAt < refreshMs) {
|
|
229
|
+
return rankTelevisionResults(cached.entries, query, maxResults);
|
|
84
230
|
}
|
|
85
231
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
232
|
+
if (cached?.pending) {
|
|
233
|
+
const entries = await cached.pending;
|
|
234
|
+
return rankTelevisionResults(entries, query, maxResults);
|
|
235
|
+
}
|
|
90
236
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
child.on("close", (code, signal) => {
|
|
115
|
-
if (settled) return;
|
|
116
|
-
if (signal) {
|
|
117
|
-
settle({ status: "cancelled" });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (code === 0) {
|
|
121
|
-
const path = cleanSelectedPath(stdout);
|
|
122
|
-
settle(path ? { status: "selected", path } : { status: "cancelled" });
|
|
123
|
-
return;
|
|
237
|
+
const fdArgs = includeFolders
|
|
238
|
+
? ["--hidden", "--follow", "--exclude", ".git", "--strip-cwd-prefix"]
|
|
239
|
+
: [
|
|
240
|
+
"--type",
|
|
241
|
+
"f",
|
|
242
|
+
"--hidden",
|
|
243
|
+
"--follow",
|
|
244
|
+
"--exclude",
|
|
245
|
+
".git",
|
|
246
|
+
"--strip-cwd-prefix",
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const pending = (async () => {
|
|
250
|
+
const result = await pi.exec("fd", fdArgs, {
|
|
251
|
+
cwd,
|
|
252
|
+
signal,
|
|
253
|
+
timeout: 10_000,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (result.code !== 0) {
|
|
257
|
+
const details = result.stderr.trim() || `exit code ${result.code}`;
|
|
258
|
+
throw new Error(`television: fd failed: ${details}`);
|
|
124
259
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
260
|
+
|
|
261
|
+
const entries = cleanPaths(result.stdout);
|
|
262
|
+
cache.set(cwd, { loadedAt: Date.now(), entries });
|
|
263
|
+
return entries;
|
|
264
|
+
})();
|
|
265
|
+
|
|
266
|
+
cache.set(cwd, { loadedAt: now, pending });
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const entries = await pending;
|
|
270
|
+
return rankTelevisionResults(entries, query, maxResults);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
cache.delete(cwd);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createTelevisionAutocompleteProvider(
|
|
279
|
+
current: AutocompleteProvider,
|
|
280
|
+
searcher: TelevisionSearcher,
|
|
281
|
+
cwd: string,
|
|
282
|
+
config: TelevisionResolvedConfig,
|
|
283
|
+
): AutocompleteProvider {
|
|
284
|
+
return {
|
|
285
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
286
|
+
const line = lines[cursorLine] ?? "";
|
|
287
|
+
const textBeforeCursor = line.slice(0, cursorCol);
|
|
288
|
+
const token = extractFileToken(textBeforeCursor);
|
|
289
|
+
|
|
290
|
+
if (token === undefined) {
|
|
291
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
128
292
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
293
|
+
|
|
294
|
+
const results = await searcher({
|
|
295
|
+
cwd,
|
|
296
|
+
query: token,
|
|
297
|
+
signal: options.signal,
|
|
298
|
+
maxResults: config.maxResults,
|
|
299
|
+
refreshMs: config.refreshMs,
|
|
300
|
+
includeFolders: config.includeFolders,
|
|
132
301
|
});
|
|
133
|
-
|
|
134
|
-
|
|
302
|
+
|
|
303
|
+
if (options.signal.aborted || results.length === 0) {
|
|
304
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
prefix: `@${token}`,
|
|
309
|
+
items: results.slice(0, config.maxResults).map(toAutocompleteItem),
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
314
|
+
return current.applyCompletion(
|
|
315
|
+
lines,
|
|
316
|
+
cursorLine,
|
|
317
|
+
cursorCol,
|
|
318
|
+
item,
|
|
319
|
+
prefix,
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
324
|
+
return (
|
|
325
|
+
current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ??
|
|
326
|
+
true
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
};
|
|
135
330
|
}
|
|
136
331
|
|
|
137
332
|
export function toEditorAttachmentPath(
|
|
@@ -161,17 +356,25 @@ function isBoundaryKey(data: string): boolean {
|
|
|
161
356
|
);
|
|
162
357
|
}
|
|
163
358
|
|
|
164
|
-
async function
|
|
359
|
+
async function findFiles(
|
|
165
360
|
ctx: ExtensionContext,
|
|
166
|
-
|
|
361
|
+
searcher: TelevisionSearcher,
|
|
167
362
|
query: string | undefined,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
ctx.ui.
|
|
363
|
+
config: TelevisionResolvedConfig,
|
|
364
|
+
): Promise<TelevisionSearchResult[]> {
|
|
365
|
+
ctx.ui.setStatus(STATUS_KEY, "television: finding files");
|
|
366
|
+
ctx.ui.setWorkingMessage("television is finding files");
|
|
171
367
|
ctx.ui.setWorkingIndicator({ frames: ["tv"], intervalMs: 1000 });
|
|
172
368
|
|
|
173
369
|
try {
|
|
174
|
-
return await
|
|
370
|
+
return await searcher({
|
|
371
|
+
cwd: ctx.cwd,
|
|
372
|
+
query,
|
|
373
|
+
signal: ctx.signal,
|
|
374
|
+
maxResults: config.maxResults,
|
|
375
|
+
refreshMs: config.refreshMs,
|
|
376
|
+
includeFolders: config.includeFolders,
|
|
377
|
+
});
|
|
175
378
|
} finally {
|
|
176
379
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
177
380
|
ctx.ui.setWorkingMessage();
|
|
@@ -179,26 +382,43 @@ async function pickFile(
|
|
|
179
382
|
}
|
|
180
383
|
}
|
|
181
384
|
|
|
182
|
-
async function
|
|
385
|
+
async function pickFileWithSelectDialog(
|
|
183
386
|
ctx: ExtensionContext,
|
|
184
|
-
|
|
387
|
+
searcher: TelevisionSearcher,
|
|
185
388
|
query: string | undefined,
|
|
389
|
+
config: TelevisionResolvedConfig,
|
|
186
390
|
): Promise<TelevisionPickResult> {
|
|
187
|
-
|
|
391
|
+
try {
|
|
392
|
+
const results = await findFiles(ctx, searcher, query, config);
|
|
188
393
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
394
|
+
if (results.length === 0) {
|
|
395
|
+
ctx.ui.notify("television: no matching files found", "warning");
|
|
396
|
+
return { status: "cancelled" };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const choice = await ctx.ui.select(
|
|
400
|
+
"television",
|
|
401
|
+
results.slice(0, config.maxResults).map((result) => result.path),
|
|
402
|
+
);
|
|
194
403
|
|
|
195
|
-
|
|
404
|
+
if (!choice) {
|
|
405
|
+
return { status: "cancelled" };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
ctx.ui.pasteToEditor(toEditorAttachmentPath(choice, ctx.cwd));
|
|
409
|
+
return { status: "selected", path: choice };
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
ctx.ui.notify(message, "error");
|
|
413
|
+
return { status: "failed", message };
|
|
414
|
+
}
|
|
196
415
|
}
|
|
197
416
|
|
|
198
417
|
function installShortcut(
|
|
199
418
|
ctx: ExtensionContext,
|
|
200
|
-
|
|
419
|
+
searcher: TelevisionSearcher,
|
|
201
420
|
shortcut: string,
|
|
421
|
+
config: TelevisionResolvedConfig,
|
|
202
422
|
): void {
|
|
203
423
|
let pickerOpen = false;
|
|
204
424
|
let lastKeyWasBoundary = true;
|
|
@@ -218,7 +438,12 @@ function installShortcut(
|
|
|
218
438
|
}
|
|
219
439
|
|
|
220
440
|
pickerOpen = true;
|
|
221
|
-
void
|
|
441
|
+
void pickFileWithSelectDialog(
|
|
442
|
+
ctx,
|
|
443
|
+
searcher,
|
|
444
|
+
query || undefined,
|
|
445
|
+
config,
|
|
446
|
+
).finally(() => {
|
|
222
447
|
pickerOpen = false;
|
|
223
448
|
lastKeyWasBoundary = true;
|
|
224
449
|
});
|
|
@@ -232,20 +457,49 @@ function installShortcut(
|
|
|
232
457
|
export function createExtension(options: TelevisionExtensionOptions = {}) {
|
|
233
458
|
const commandName = options.commandName ?? DEFAULT_COMMAND_NAME;
|
|
234
459
|
const shortcut = options.shortcut ?? DEFAULT_SHORTCUT;
|
|
235
|
-
const runner = options.runner ?? runTelevision;
|
|
236
460
|
|
|
237
461
|
return {
|
|
238
462
|
name: extensionInfo.name,
|
|
239
463
|
register(pi: ExtensionAPI): void {
|
|
240
|
-
|
|
241
|
-
|
|
464
|
+
const searcher = options.searcher ?? createDefaultSearcher(pi);
|
|
465
|
+
const configLoader = options.configLoader ?? loadTelevisionConfig;
|
|
466
|
+
let config = defaultResolvedConfig;
|
|
467
|
+
|
|
468
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
469
|
+
try {
|
|
470
|
+
config = await configLoader(ctx.cwd);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const message =
|
|
473
|
+
error instanceof Error ? error.message : String(error);
|
|
474
|
+
config = defaultResolvedConfig;
|
|
475
|
+
ctx.ui.notify(message, "error");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (config.mode === "native-live") {
|
|
479
|
+
ctx.ui.addAutocompleteProvider((current) =>
|
|
480
|
+
createTelevisionAutocompleteProvider(
|
|
481
|
+
current,
|
|
482
|
+
searcher,
|
|
483
|
+
ctx.cwd,
|
|
484
|
+
config,
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
installShortcut(ctx, searcher, shortcut, config);
|
|
242
491
|
});
|
|
243
492
|
|
|
244
493
|
pi.registerCommand(commandName, {
|
|
245
494
|
description:
|
|
246
|
-
"
|
|
495
|
+
"Find a file in the background and insert it as an @file attachment",
|
|
247
496
|
handler: async (args, ctx) => {
|
|
248
|
-
await
|
|
497
|
+
await pickFileWithSelectDialog(
|
|
498
|
+
ctx,
|
|
499
|
+
searcher,
|
|
500
|
+
args.trim() || undefined,
|
|
501
|
+
config,
|
|
502
|
+
);
|
|
249
503
|
},
|
|
250
504
|
});
|
|
251
505
|
},
|
package/src/index.ts
CHANGED
|
@@ -2,14 +2,23 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { createExtension } from "./extension.ts";
|
|
3
3
|
|
|
4
4
|
export type {
|
|
5
|
+
TelevisionConfig,
|
|
6
|
+
TelevisionConfigLoader,
|
|
5
7
|
TelevisionExtensionOptions,
|
|
8
|
+
TelevisionMode,
|
|
6
9
|
TelevisionPickResult,
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
TelevisionResolvedConfig,
|
|
11
|
+
TelevisionSearcher,
|
|
12
|
+
TelevisionSearchOptions,
|
|
13
|
+
TelevisionSearchResult,
|
|
9
14
|
} from "./extension.ts";
|
|
10
15
|
export {
|
|
16
|
+
createDefaultSearcher,
|
|
11
17
|
createExtension,
|
|
18
|
+
createTelevisionAutocompleteProvider,
|
|
12
19
|
extensionInfo,
|
|
20
|
+
loadTelevisionConfig,
|
|
21
|
+
rankTelevisionResults,
|
|
13
22
|
toEditorAttachmentPath,
|
|
14
23
|
} from "./extension.ts";
|
|
15
24
|
|