@ramarivera/pi-television 0.0.2 → 0.0.4
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 +46 -3
- package/package.json +2 -2
- package/src/extension.ts +351 -108
- package/src/index.ts +11 -2
package/README.md
CHANGED
|
@@ -1,11 +1,55 @@
|
|
|
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
|
+
```
|
|
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
|
+
"maxResults": 20,
|
|
51
|
+
"refreshMs": 10000
|
|
52
|
+
}
|
|
9
53
|
```
|
|
10
54
|
|
|
11
55
|
## Local Development
|
|
@@ -37,4 +81,3 @@ Before the first publish, configure npm trusted publishing:
|
|
|
37
81
|
- environment: blank unless the workflow is changed to require one
|
|
38
82
|
|
|
39
83
|
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.4",
|
|
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,296 @@ 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
|
+
};
|
|
37
|
+
|
|
38
|
+
export type TelevisionResolvedConfig = {
|
|
39
|
+
mode: TelevisionMode;
|
|
40
|
+
maxResults: number;
|
|
41
|
+
refreshMs: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type TelevisionSearchResult = {
|
|
45
|
+
path: string;
|
|
46
|
+
label?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type TelevisionSearchOptions = {
|
|
21
51
|
cwd: string;
|
|
22
52
|
query?: string;
|
|
23
53
|
signal?: AbortSignal;
|
|
54
|
+
maxResults?: number;
|
|
55
|
+
refreshMs?: number;
|
|
24
56
|
};
|
|
25
57
|
|
|
26
|
-
export type
|
|
27
|
-
options:
|
|
28
|
-
) => Promise<
|
|
58
|
+
export type TelevisionSearcher = (
|
|
59
|
+
options: TelevisionSearchOptions,
|
|
60
|
+
) => Promise<TelevisionSearchResult[]>;
|
|
61
|
+
|
|
62
|
+
export type TelevisionConfigLoader = (
|
|
63
|
+
cwd: string,
|
|
64
|
+
) => Promise<TelevisionResolvedConfig>;
|
|
29
65
|
|
|
30
66
|
export type TelevisionExtensionOptions = {
|
|
31
67
|
commandName?: string;
|
|
32
68
|
shortcut?: string;
|
|
33
|
-
|
|
69
|
+
searcher?: TelevisionSearcher;
|
|
70
|
+
configLoader?: TelevisionConfigLoader;
|
|
34
71
|
};
|
|
35
72
|
|
|
36
73
|
const STATUS_KEY = "pi-television";
|
|
37
74
|
const DEFAULT_COMMAND_NAME = "television";
|
|
38
75
|
const DEFAULT_SHORTCUT = "@";
|
|
76
|
+
const DEFAULT_MAX_RESULTS = 20;
|
|
77
|
+
const DEFAULT_REFRESH_MS = 10_000;
|
|
39
78
|
|
|
40
79
|
export const extensionInfo: ExtensionInfo = {
|
|
41
80
|
name: "television",
|
|
42
81
|
description:
|
|
43
|
-
"Pi extension that
|
|
82
|
+
"Pi extension that powers native @file picking with background television-style search",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const defaultResolvedConfig: TelevisionResolvedConfig = {
|
|
86
|
+
mode: "native-live",
|
|
87
|
+
maxResults: DEFAULT_MAX_RESULTS,
|
|
88
|
+
refreshMs: DEFAULT_REFRESH_MS,
|
|
44
89
|
};
|
|
45
90
|
|
|
46
|
-
|
|
47
|
-
|
|
91
|
+
type FileIndexCache = {
|
|
92
|
+
loadedAt: number;
|
|
93
|
+
entries?: string[];
|
|
94
|
+
pending?: Promise<string[]>;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function uniq(values: string[]): string[] {
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const deduped: string[] = [];
|
|
100
|
+
|
|
101
|
+
for (const value of values) {
|
|
102
|
+
if (seen.has(value)) continue;
|
|
103
|
+
seen.add(value);
|
|
104
|
+
deduped.push(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return deduped;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cleanPaths(stdout: string): string[] {
|
|
111
|
+
return stdout
|
|
48
112
|
.split(/\r?\n/)
|
|
49
113
|
.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
|
-
|
|
114
|
+
.filter((line) => line.length > 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractFileToken(textBeforeCursor: string): string | undefined {
|
|
118
|
+
const match = textBeforeCursor.match(/(?:^|[ \t])@([^\s@]*)$/);
|
|
119
|
+
return match?.[1];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function toAutocompleteItem(result: TelevisionSearchResult): AutocompleteItem {
|
|
123
|
+
return {
|
|
124
|
+
value: `@${result.path}`,
|
|
125
|
+
label: result.label ?? result.path,
|
|
126
|
+
description: result.description ?? result.path,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeTelevisionConfig(
|
|
131
|
+
config: TelevisionConfig | undefined,
|
|
132
|
+
): TelevisionResolvedConfig {
|
|
133
|
+
return {
|
|
134
|
+
mode: config?.mode ?? defaultResolvedConfig.mode,
|
|
135
|
+
maxResults: config?.maxResults ?? defaultResolvedConfig.maxResults,
|
|
136
|
+
refreshMs: config?.refreshMs ?? defaultResolvedConfig.refreshMs,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function loadConfigFile(
|
|
141
|
+
path: string,
|
|
142
|
+
): Promise<TelevisionConfig | undefined> {
|
|
143
|
+
try {
|
|
144
|
+
const content = await readFile(path, "utf8");
|
|
145
|
+
const parsed = JSON.parse(content) as TelevisionConfig;
|
|
146
|
+
return parsed;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
149
|
+
if (nodeError.code === "ENOENT") {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`television: failed to read ${path}: ${nodeError.message}`);
|
|
72
153
|
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function loadTelevisionConfig(
|
|
157
|
+
cwd: string,
|
|
158
|
+
): Promise<TelevisionResolvedConfig> {
|
|
159
|
+
const globalConfig = await loadConfigFile(
|
|
160
|
+
resolve(homedir(), ".pi", "agent", "television.json"),
|
|
161
|
+
);
|
|
162
|
+
const projectConfig = await loadConfigFile(
|
|
163
|
+
resolve(cwd, ".pi", "television.json"),
|
|
164
|
+
);
|
|
73
165
|
|
|
74
|
-
return
|
|
166
|
+
return normalizeTelevisionConfig({
|
|
167
|
+
...globalConfig,
|
|
168
|
+
...projectConfig,
|
|
169
|
+
});
|
|
75
170
|
}
|
|
76
171
|
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
172
|
+
export function rankTelevisionResults(
|
|
173
|
+
paths: string[],
|
|
174
|
+
query: string | undefined,
|
|
175
|
+
maxResults: number,
|
|
176
|
+
): TelevisionSearchResult[] {
|
|
177
|
+
const trimmedQuery = query?.trim() ?? "";
|
|
178
|
+
const limitedMaxResults = Math.max(1, maxResults);
|
|
179
|
+
|
|
180
|
+
if (!trimmedQuery) {
|
|
181
|
+
return paths.slice(0, limitedMaxResults).map((path) => ({ path }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const exactPrefixMatches = paths.filter((path) =>
|
|
185
|
+
path.startsWith(trimmedQuery),
|
|
186
|
+
);
|
|
187
|
+
const basenamePrefixMatches = paths.filter(
|
|
188
|
+
(path) =>
|
|
189
|
+
basename(path).startsWith(trimmedQuery) &&
|
|
190
|
+
!exactPrefixMatches.includes(path),
|
|
191
|
+
);
|
|
192
|
+
const fuzzyMatches = fuzzyFilter(paths, trimmedQuery, (path) => {
|
|
193
|
+
const name = basename(path);
|
|
194
|
+
return name === path ? path : `${name} ${path}`;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return uniq([
|
|
198
|
+
...exactPrefixMatches,
|
|
199
|
+
...basenamePrefixMatches,
|
|
200
|
+
...fuzzyMatches,
|
|
201
|
+
])
|
|
202
|
+
.slice(0, limitedMaxResults)
|
|
203
|
+
.map((path) => ({ path }));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createDefaultSearcher(pi: ExtensionAPI): TelevisionSearcher {
|
|
207
|
+
const cache = new Map<string, FileIndexCache>();
|
|
208
|
+
|
|
209
|
+
return async ({
|
|
210
|
+
cwd,
|
|
211
|
+
query,
|
|
212
|
+
signal,
|
|
213
|
+
maxResults = DEFAULT_MAX_RESULTS,
|
|
214
|
+
refreshMs = DEFAULT_REFRESH_MS,
|
|
215
|
+
}) => {
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const cached = cache.get(cwd);
|
|
218
|
+
|
|
219
|
+
if (cached?.entries && now - cached.loadedAt < refreshMs) {
|
|
220
|
+
return rankTelevisionResults(cached.entries, query, maxResults);
|
|
84
221
|
}
|
|
85
222
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
223
|
+
if (cached?.pending) {
|
|
224
|
+
const entries = await cached.pending;
|
|
225
|
+
return rankTelevisionResults(entries, query, maxResults);
|
|
226
|
+
}
|
|
90
227
|
|
|
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;
|
|
228
|
+
const pending = (async () => {
|
|
229
|
+
const result = await pi.exec(
|
|
230
|
+
"fd",
|
|
231
|
+
[
|
|
232
|
+
"--type",
|
|
233
|
+
"f",
|
|
234
|
+
"--hidden",
|
|
235
|
+
"--follow",
|
|
236
|
+
"--exclude",
|
|
237
|
+
".git",
|
|
238
|
+
"--strip-cwd-prefix",
|
|
239
|
+
],
|
|
240
|
+
{
|
|
241
|
+
cwd,
|
|
242
|
+
signal,
|
|
243
|
+
timeout: 10_000,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (result.code !== 0) {
|
|
248
|
+
const details = result.stderr.trim() || `exit code ${result.code}`;
|
|
249
|
+
throw new Error(`television: fd failed: ${details}`);
|
|
124
250
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
251
|
+
|
|
252
|
+
const entries = cleanPaths(result.stdout);
|
|
253
|
+
cache.set(cwd, { loadedAt: Date.now(), entries });
|
|
254
|
+
return entries;
|
|
255
|
+
})();
|
|
256
|
+
|
|
257
|
+
cache.set(cwd, { loadedAt: now, pending });
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const entries = await pending;
|
|
261
|
+
return rankTelevisionResults(entries, query, maxResults);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
cache.delete(cwd);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createTelevisionAutocompleteProvider(
|
|
270
|
+
current: AutocompleteProvider,
|
|
271
|
+
searcher: TelevisionSearcher,
|
|
272
|
+
cwd: string,
|
|
273
|
+
config: TelevisionResolvedConfig,
|
|
274
|
+
): AutocompleteProvider {
|
|
275
|
+
return {
|
|
276
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
277
|
+
const line = lines[cursorLine] ?? "";
|
|
278
|
+
const textBeforeCursor = line.slice(0, cursorCol);
|
|
279
|
+
const token = extractFileToken(textBeforeCursor);
|
|
280
|
+
|
|
281
|
+
if (token === undefined) {
|
|
282
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
128
283
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
284
|
+
|
|
285
|
+
const results = await searcher({
|
|
286
|
+
cwd,
|
|
287
|
+
query: token,
|
|
288
|
+
signal: options.signal,
|
|
289
|
+
maxResults: config.maxResults,
|
|
290
|
+
refreshMs: config.refreshMs,
|
|
132
291
|
});
|
|
133
|
-
|
|
134
|
-
|
|
292
|
+
|
|
293
|
+
if (options.signal.aborted || results.length === 0) {
|
|
294
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
prefix: `@${token}`,
|
|
299
|
+
items: results.slice(0, config.maxResults).map(toAutocompleteItem),
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
304
|
+
return current.applyCompletion(
|
|
305
|
+
lines,
|
|
306
|
+
cursorLine,
|
|
307
|
+
cursorCol,
|
|
308
|
+
item,
|
|
309
|
+
prefix,
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
314
|
+
return (
|
|
315
|
+
current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ??
|
|
316
|
+
true
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
135
320
|
}
|
|
136
321
|
|
|
137
322
|
export function toEditorAttachmentPath(
|
|
@@ -161,17 +346,24 @@ function isBoundaryKey(data: string): boolean {
|
|
|
161
346
|
);
|
|
162
347
|
}
|
|
163
348
|
|
|
164
|
-
async function
|
|
349
|
+
async function findFiles(
|
|
165
350
|
ctx: ExtensionContext,
|
|
166
|
-
|
|
351
|
+
searcher: TelevisionSearcher,
|
|
167
352
|
query: string | undefined,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
ctx.ui.
|
|
353
|
+
config: TelevisionResolvedConfig,
|
|
354
|
+
): Promise<TelevisionSearchResult[]> {
|
|
355
|
+
ctx.ui.setStatus(STATUS_KEY, "television: finding files");
|
|
356
|
+
ctx.ui.setWorkingMessage("television is finding files");
|
|
171
357
|
ctx.ui.setWorkingIndicator({ frames: ["tv"], intervalMs: 1000 });
|
|
172
358
|
|
|
173
359
|
try {
|
|
174
|
-
return await
|
|
360
|
+
return await searcher({
|
|
361
|
+
cwd: ctx.cwd,
|
|
362
|
+
query,
|
|
363
|
+
signal: ctx.signal,
|
|
364
|
+
maxResults: config.maxResults,
|
|
365
|
+
refreshMs: config.refreshMs,
|
|
366
|
+
});
|
|
175
367
|
} finally {
|
|
176
368
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
177
369
|
ctx.ui.setWorkingMessage();
|
|
@@ -179,26 +371,43 @@ async function pickFile(
|
|
|
179
371
|
}
|
|
180
372
|
}
|
|
181
373
|
|
|
182
|
-
async function
|
|
374
|
+
async function pickFileWithSelectDialog(
|
|
183
375
|
ctx: ExtensionContext,
|
|
184
|
-
|
|
376
|
+
searcher: TelevisionSearcher,
|
|
185
377
|
query: string | undefined,
|
|
378
|
+
config: TelevisionResolvedConfig,
|
|
186
379
|
): Promise<TelevisionPickResult> {
|
|
187
|
-
|
|
380
|
+
try {
|
|
381
|
+
const results = await findFiles(ctx, searcher, query, config);
|
|
188
382
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
383
|
+
if (results.length === 0) {
|
|
384
|
+
ctx.ui.notify("television: no matching files found", "warning");
|
|
385
|
+
return { status: "cancelled" };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const choice = await ctx.ui.select(
|
|
389
|
+
"television",
|
|
390
|
+
results.slice(0, config.maxResults).map((result) => result.path),
|
|
391
|
+
);
|
|
194
392
|
|
|
195
|
-
|
|
393
|
+
if (!choice) {
|
|
394
|
+
return { status: "cancelled" };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
ctx.ui.pasteToEditor(toEditorAttachmentPath(choice, ctx.cwd));
|
|
398
|
+
return { status: "selected", path: choice };
|
|
399
|
+
} catch (error) {
|
|
400
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
401
|
+
ctx.ui.notify(message, "error");
|
|
402
|
+
return { status: "failed", message };
|
|
403
|
+
}
|
|
196
404
|
}
|
|
197
405
|
|
|
198
406
|
function installShortcut(
|
|
199
407
|
ctx: ExtensionContext,
|
|
200
|
-
|
|
408
|
+
searcher: TelevisionSearcher,
|
|
201
409
|
shortcut: string,
|
|
410
|
+
config: TelevisionResolvedConfig,
|
|
202
411
|
): void {
|
|
203
412
|
let pickerOpen = false;
|
|
204
413
|
let lastKeyWasBoundary = true;
|
|
@@ -218,7 +427,12 @@ function installShortcut(
|
|
|
218
427
|
}
|
|
219
428
|
|
|
220
429
|
pickerOpen = true;
|
|
221
|
-
void
|
|
430
|
+
void pickFileWithSelectDialog(
|
|
431
|
+
ctx,
|
|
432
|
+
searcher,
|
|
433
|
+
query || undefined,
|
|
434
|
+
config,
|
|
435
|
+
).finally(() => {
|
|
222
436
|
pickerOpen = false;
|
|
223
437
|
lastKeyWasBoundary = true;
|
|
224
438
|
});
|
|
@@ -232,20 +446,49 @@ function installShortcut(
|
|
|
232
446
|
export function createExtension(options: TelevisionExtensionOptions = {}) {
|
|
233
447
|
const commandName = options.commandName ?? DEFAULT_COMMAND_NAME;
|
|
234
448
|
const shortcut = options.shortcut ?? DEFAULT_SHORTCUT;
|
|
235
|
-
const runner = options.runner ?? runTelevision;
|
|
236
449
|
|
|
237
450
|
return {
|
|
238
451
|
name: extensionInfo.name,
|
|
239
452
|
register(pi: ExtensionAPI): void {
|
|
240
|
-
|
|
241
|
-
|
|
453
|
+
const searcher = options.searcher ?? createDefaultSearcher(pi);
|
|
454
|
+
const configLoader = options.configLoader ?? loadTelevisionConfig;
|
|
455
|
+
let config = defaultResolvedConfig;
|
|
456
|
+
|
|
457
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
458
|
+
try {
|
|
459
|
+
config = await configLoader(ctx.cwd);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
const message =
|
|
462
|
+
error instanceof Error ? error.message : String(error);
|
|
463
|
+
config = defaultResolvedConfig;
|
|
464
|
+
ctx.ui.notify(message, "error");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (config.mode === "native-live") {
|
|
468
|
+
ctx.ui.addAutocompleteProvider((current) =>
|
|
469
|
+
createTelevisionAutocompleteProvider(
|
|
470
|
+
current,
|
|
471
|
+
searcher,
|
|
472
|
+
ctx.cwd,
|
|
473
|
+
config,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
installShortcut(ctx, searcher, shortcut, config);
|
|
242
480
|
});
|
|
243
481
|
|
|
244
482
|
pi.registerCommand(commandName, {
|
|
245
483
|
description:
|
|
246
|
-
"
|
|
484
|
+
"Find a file in the background and insert it as an @file attachment",
|
|
247
485
|
handler: async (args, ctx) => {
|
|
248
|
-
await
|
|
486
|
+
await pickFileWithSelectDialog(
|
|
487
|
+
ctx,
|
|
488
|
+
searcher,
|
|
489
|
+
args.trim() || undefined,
|
|
490
|
+
config,
|
|
491
|
+
);
|
|
249
492
|
},
|
|
250
493
|
});
|
|
251
494
|
},
|
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
|
|