@aaroncql/pim-agent 0.1.0 → 0.3.0

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 (38) hide show
  1. package/README.md +92 -65
  2. package/package.json +6 -6
  3. package/src/extensions/apply-patch/coordinator.ts +67 -0
  4. package/src/extensions/apply-patch/executor.ts +566 -0
  5. package/src/extensions/apply-patch/index.ts +75 -0
  6. package/src/extensions/apply-patch/matcher.ts +66 -0
  7. package/src/extensions/apply-patch/model.ts +34 -0
  8. package/src/extensions/apply-patch/parser.ts +381 -0
  9. package/src/extensions/apply-patch/render.ts +261 -0
  10. package/src/extensions/apply-patch/schema.ts +43 -0
  11. package/src/extensions/apply-patch/types.ts +30 -0
  12. package/src/extensions/bash/index.ts +3 -3
  13. package/src/extensions/edit/index.ts +2 -1
  14. package/src/extensions/file-picker/FilePickerSuggestionEngine.ts +14 -0
  15. package/src/extensions/file-picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  16. package/src/extensions/file-picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  17. package/src/extensions/file-picker/catalog.ts +38 -33
  18. package/src/extensions/file-picker/filePickerWorker.ts +72 -0
  19. package/src/extensions/file-picker/filePickerWorkerMessages.ts +39 -0
  20. package/src/extensions/file-picker/index.ts +138 -83
  21. package/src/extensions/file-picker/ranker.ts +180 -12
  22. package/src/extensions/glob/index.ts +3 -1
  23. package/src/extensions/glob/schema.ts +2 -1
  24. package/src/extensions/grep/grep.ts +45 -2
  25. package/src/extensions/grep/index.ts +3 -1
  26. package/src/extensions/grep/render.ts +18 -4
  27. package/src/extensions/grep/schema.ts +1 -1
  28. package/src/extensions/read/index.ts +36 -9
  29. package/src/extensions/read/render.ts +31 -3
  30. package/src/extensions/subagent/index.ts +4 -1
  31. package/src/extensions/todo/index.ts +4 -3
  32. package/src/extensions/web-search/index.ts +2 -1
  33. package/src/extensions/write/index.ts +2 -1
  34. package/src/shared/FileEnumerator.ts +492 -0
  35. package/src/shared/FileScanner.ts +15 -17
  36. package/src/shared/PatchSummary.ts +82 -0
  37. package/src/telegram/Renderer.ts +190 -4
  38. package/src/shared/GitignoreFilter.ts +0 -142
@@ -0,0 +1,43 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const applyPatchSchema = Type.Object({
4
+ input: Type.String({
5
+ description: "Patch text wrapped in *** Begin Patch / *** End Patch.",
6
+ }),
7
+ });
8
+
9
+ export type ApplyPatchInput = Static<typeof applyPatchSchema>;
10
+
11
+ const ALIAS_KEYS = ["patch", "patchText", "patch_text"] as const;
12
+
13
+ /**
14
+ * Forgive the JSON-key choice, trust the grammar. Accepts `{input}` (canonical,
15
+ * handled above), `{patch}`, `{patchText}`/`{patch_text}`, or a bare string,
16
+ * normalizing to `{input}` and stripping the alias key so the unknown-key
17
+ * rejection in `Tools.wrap` passes. Validation of the actual envelope happens
18
+ * in the parser.
19
+ */
20
+ export function prepareApplyPatchArguments(rawArgs: unknown): ApplyPatchInput {
21
+ if (typeof rawArgs === "string") {
22
+ return { input: rawArgs };
23
+ }
24
+
25
+ if (rawArgs === null || typeof rawArgs !== "object") {
26
+ return rawArgs as ApplyPatchInput;
27
+ }
28
+
29
+ const record = rawArgs as Record<string, unknown>;
30
+ if (typeof record.input === "string") {
31
+ return rawArgs as ApplyPatchInput;
32
+ }
33
+
34
+ for (const key of ALIAS_KEYS) {
35
+ const value = record[key];
36
+ if (typeof value === "string") {
37
+ const { [key]: _dropped, ...rest } = record;
38
+ return { ...rest, input: value } as ApplyPatchInput;
39
+ }
40
+ }
41
+
42
+ return rawArgs as ApplyPatchInput;
43
+ }
@@ -0,0 +1,30 @@
1
+ export type UpdateChunk = {
2
+ readonly changeContext: string | undefined;
3
+ readonly oldLines: readonly string[];
4
+ readonly newLines: readonly string[];
5
+ readonly isEndOfFile: boolean;
6
+ };
7
+
8
+ export type AddHunk = {
9
+ readonly kind: "add";
10
+ readonly path: string;
11
+ readonly contents: string;
12
+ };
13
+
14
+ export type DeleteHunk = {
15
+ readonly kind: "delete";
16
+ readonly path: string;
17
+ };
18
+
19
+ export type UpdateHunk = {
20
+ readonly kind: "update";
21
+ readonly path: string;
22
+ readonly movePath: string | undefined;
23
+ readonly chunks: readonly UpdateChunk[];
24
+ };
25
+
26
+ export type Hunk = AddHunk | DeleteHunk | UpdateHunk;
27
+
28
+ export type Patch = {
29
+ readonly hunks: readonly Hunk[];
30
+ };
@@ -37,9 +37,9 @@ export default function (pi: ExtensionAPI): void {
37
37
  name: "bash",
38
38
  label: "bash",
39
39
  description:
40
- `Execute a bash command in the cwd. ` +
41
- `Returns exit code, signal (if any), and stdout/stderr captured separately. ` +
42
- `Prefer commands that emit only what you need; keep output as small as possible.`,
40
+ "Execute a bash command in the cwd. " +
41
+ "Returns exit code, signal (if any), and stdout/stderr captured separately. " +
42
+ "Prefer commands that emit only what you need; keep output as small as possible.",
43
43
  parameters: bashSchema,
44
44
  renderShell: "self",
45
45
  executionMode: "sequential",
@@ -12,7 +12,8 @@ export default function (pi: ExtensionAPI): void {
12
12
  name: "edit",
13
13
  label: "edit",
14
14
  description:
15
- "Replace strings in a UTF-8 text file. Prefer edit over write for changes to existing files.",
15
+ "Replace strings in a UTF-8 text file. " +
16
+ "Prefer edit over write for changes to existing files.",
16
17
  parameters: editSchema,
17
18
  renderShell: "self",
18
19
  executionMode: "sequential",
@@ -0,0 +1,14 @@
1
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
+
3
+ export type RankFilePickerOptions = {
4
+ readonly limit?: number;
5
+ readonly signal?: AbortSignal;
6
+ };
7
+
8
+ export type FilePickerSuggestionEngine = {
9
+ readonly refreshRelative: () => Promise<void>;
10
+ readonly rank: (
11
+ query: string,
12
+ options: RankFilePickerOptions
13
+ ) => Promise<readonly AutocompleteItem[] | undefined>;
14
+ };
@@ -0,0 +1,52 @@
1
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
+ import type { FileCandidate } from "./catalog";
3
+ import type {
4
+ FilePickerSuggestionEngine,
5
+ RankFilePickerOptions,
6
+ } from "./FilePickerSuggestionEngine";
7
+ import { rank } from "./ranker";
8
+
9
+ export type InProcessFilePickerSuggestionEngineOptions = {
10
+ readonly loadRelativeCatalog: () => Promise<readonly FileCandidate[]>;
11
+ };
12
+
13
+ export class InProcessFilePickerSuggestionEngine implements FilePickerSuggestionEngine {
14
+ private cachedRelative: readonly FileCandidate[] | undefined;
15
+ private refresh: Promise<void> | undefined;
16
+
17
+ public constructor(
18
+ private readonly options: InProcessFilePickerSuggestionEngineOptions
19
+ ) {}
20
+
21
+ public refreshRelative(): Promise<void> {
22
+ this.refresh ??= this.options
23
+ .loadRelativeCatalog()
24
+ .then((catalog) => {
25
+ this.cachedRelative = catalog;
26
+ })
27
+ .catch(() => {
28
+ if (this.cachedRelative === undefined) {
29
+ this.cachedRelative = [];
30
+ }
31
+ })
32
+ .finally(() => {
33
+ this.refresh = undefined;
34
+ });
35
+
36
+ return this.refresh;
37
+ }
38
+
39
+ public async rank(
40
+ query: string,
41
+ options: RankFilePickerOptions
42
+ ): Promise<readonly AutocompleteItem[] | undefined> {
43
+ if (options.signal?.aborted === true) {
44
+ return [];
45
+ }
46
+
47
+ return rank(query, {
48
+ cachedRelative: this.cachedRelative,
49
+ limit: options.limit,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,268 @@
1
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
+ import type {
3
+ FilePickerSuggestionEngine,
4
+ RankFilePickerOptions,
5
+ } from "./FilePickerSuggestionEngine";
6
+ import type {
7
+ FilePickerWorkerRequest,
8
+ FilePickerWorkerResponse,
9
+ } from "./filePickerWorkerMessages";
10
+
11
+ type PendingRefresh = {
12
+ readonly resolve: () => void;
13
+ };
14
+
15
+ type RankRequest = {
16
+ readonly id: number;
17
+ readonly query: string;
18
+ readonly limit: number | undefined;
19
+ readonly resolve: (items: readonly AutocompleteItem[] | undefined) => void;
20
+ readonly reject: (error: unknown) => void;
21
+ readonly signal: AbortSignal | undefined;
22
+ readonly abortListener: (() => void) | undefined;
23
+ settled: boolean;
24
+ };
25
+
26
+ export class WorkerFilePickerSuggestionEngine implements FilePickerSuggestionEngine {
27
+ private worker: Worker | undefined;
28
+ private nextRequestId = 0;
29
+ private refresh: Promise<void> | undefined;
30
+ private activeRank: RankRequest | undefined;
31
+ private queuedRank: RankRequest | undefined;
32
+ private readonly pendingRefreshes = new Map<number, PendingRefresh>();
33
+
34
+ public constructor(private readonly root: string) {}
35
+
36
+ public refreshRelative(): Promise<void> {
37
+ this.refresh ??= this.sendRefreshRelative().finally(() => {
38
+ this.refresh = undefined;
39
+ });
40
+ return this.refresh;
41
+ }
42
+
43
+ public rank(
44
+ query: string,
45
+ options: RankFilePickerOptions
46
+ ): Promise<readonly AutocompleteItem[] | undefined> {
47
+ if (options.signal?.aborted === true) {
48
+ return Promise.resolve([]);
49
+ }
50
+
51
+ const id = this.nextRequestId++;
52
+ return new Promise((resolve, reject) => {
53
+ let request: RankRequest;
54
+ const abortListener = (): void => {
55
+ this.abortRank(request);
56
+ };
57
+ request = {
58
+ id,
59
+ query,
60
+ limit: options.limit,
61
+ resolve,
62
+ reject,
63
+ signal: options.signal,
64
+ abortListener: options.signal === undefined ? undefined : abortListener,
65
+ settled: false,
66
+ };
67
+
68
+ if (options.signal !== undefined) {
69
+ options.signal.addEventListener("abort", abortListener, { once: true });
70
+ }
71
+
72
+ this.enqueueRank(request);
73
+ });
74
+ }
75
+
76
+ public dispose(): void {
77
+ this.worker?.terminate();
78
+ this.worker = undefined;
79
+ this.failPending(new Error("file picker worker disposed"));
80
+ }
81
+
82
+ private enqueueRank(request: RankRequest): void {
83
+ if (this.activeRank === undefined) {
84
+ this.sendRank(request);
85
+ return;
86
+ }
87
+
88
+ this.resolveRank(this.activeRank, []);
89
+ if (this.queuedRank !== undefined) {
90
+ this.resolveRank(this.queuedRank, []);
91
+ }
92
+ this.queuedRank = request;
93
+ }
94
+
95
+ private sendRank(request: RankRequest): void {
96
+ this.activeRank = request;
97
+ try {
98
+ this.post({
99
+ id: request.id,
100
+ type: "rank",
101
+ query: request.query,
102
+ limit: request.limit,
103
+ });
104
+ } catch (error) {
105
+ if (this.activeRank === request) {
106
+ this.activeRank = undefined;
107
+ }
108
+ this.rejectRank(request, error);
109
+ this.sendQueuedRank();
110
+ }
111
+ }
112
+
113
+ private sendQueuedRank(): void {
114
+ const request = this.queuedRank;
115
+ if (request === undefined || this.activeRank !== undefined) {
116
+ return;
117
+ }
118
+
119
+ this.queuedRank = undefined;
120
+ if (request.settled) {
121
+ this.sendQueuedRank();
122
+ return;
123
+ }
124
+
125
+ this.sendRank(request);
126
+ }
127
+
128
+ private abortRank(request: RankRequest): void {
129
+ if (this.queuedRank === request) {
130
+ this.queuedRank = undefined;
131
+ }
132
+ this.resolveRank(request, []);
133
+ }
134
+
135
+ private resolveRank(
136
+ request: RankRequest,
137
+ items: readonly AutocompleteItem[] | undefined
138
+ ): void {
139
+ if (request.settled) {
140
+ return;
141
+ }
142
+
143
+ request.settled = true;
144
+ this.removeAbortListener(request);
145
+ request.resolve(items);
146
+ }
147
+
148
+ private rejectRank(request: RankRequest, error: unknown): void {
149
+ if (request.settled) {
150
+ return;
151
+ }
152
+
153
+ request.settled = true;
154
+ this.removeAbortListener(request);
155
+ request.reject(error);
156
+ }
157
+
158
+ private sendRefreshRelative(): Promise<void> {
159
+ const id = this.nextRequestId++;
160
+ return new Promise((resolve) => {
161
+ this.pendingRefreshes.set(id, { resolve });
162
+ try {
163
+ this.post({
164
+ id,
165
+ type: "refreshRelative",
166
+ root: this.root,
167
+ });
168
+ } catch {
169
+ this.pendingRefreshes.delete(id);
170
+ resolve();
171
+ }
172
+ });
173
+ }
174
+
175
+ private post(message: FilePickerWorkerRequest): void {
176
+ this.currentWorker().postMessage(message);
177
+ }
178
+
179
+ private currentWorker(): Worker {
180
+ if (this.worker !== undefined) {
181
+ return this.worker;
182
+ }
183
+
184
+ const worker = new Worker(
185
+ new URL("./filePickerWorker.ts", import.meta.url),
186
+ {
187
+ type: "module",
188
+ }
189
+ );
190
+ worker.onmessage = (
191
+ event: MessageEvent<FilePickerWorkerResponse>
192
+ ): void => {
193
+ this.handleMessage(event.data);
194
+ };
195
+ worker.onerror = (event): void => {
196
+ this.worker = undefined;
197
+ worker.terminate();
198
+ this.failPending(new Error(event.message));
199
+ };
200
+ this.worker = worker;
201
+ return worker;
202
+ }
203
+
204
+ private handleMessage(message: FilePickerWorkerResponse): void {
205
+ switch (message.type) {
206
+ case "refreshRelative":
207
+ this.handleRefreshRelativeMessage(message);
208
+ break;
209
+ case "rank":
210
+ this.handleRankMessage(message);
211
+ break;
212
+ }
213
+ }
214
+
215
+ private handleRefreshRelativeMessage(
216
+ message: Extract<
217
+ FilePickerWorkerResponse,
218
+ { readonly type: "refreshRelative" }
219
+ >
220
+ ): void {
221
+ const pending = this.pendingRefreshes.get(message.id);
222
+ if (pending === undefined) {
223
+ return;
224
+ }
225
+
226
+ this.pendingRefreshes.delete(message.id);
227
+ pending.resolve();
228
+ }
229
+
230
+ private handleRankMessage(
231
+ message: Extract<FilePickerWorkerResponse, { readonly type: "rank" }>
232
+ ): void {
233
+ const request = this.activeRank;
234
+ if (request === undefined || request.id !== message.id) {
235
+ return;
236
+ }
237
+
238
+ this.activeRank = undefined;
239
+ if (message.ok) {
240
+ this.resolveRank(request, message.items);
241
+ } else {
242
+ this.rejectRank(request, new Error(message.error));
243
+ }
244
+ this.sendQueuedRank();
245
+ }
246
+
247
+ private failPending(error: unknown): void {
248
+ for (const [id, pending] of this.pendingRefreshes) {
249
+ this.pendingRefreshes.delete(id);
250
+ pending.resolve();
251
+ }
252
+
253
+ if (this.activeRank !== undefined) {
254
+ this.rejectRank(this.activeRank, error);
255
+ this.activeRank = undefined;
256
+ }
257
+ if (this.queuedRank !== undefined) {
258
+ this.rejectRank(this.queuedRank, error);
259
+ this.queuedRank = undefined;
260
+ }
261
+ }
262
+
263
+ private removeAbortListener(request: RankRequest): void {
264
+ if (request.signal !== undefined && request.abortListener !== undefined) {
265
+ request.signal.removeEventListener("abort", request.abortListener);
266
+ }
267
+ }
268
+ }
@@ -1,6 +1,6 @@
1
1
  import { readdir, stat } from "node:fs/promises";
2
- import { isAbsolute, join, parse, relative, resolve, sep } from "node:path";
3
- import { GitignoreFilter } from "../../shared/GitignoreFilter";
2
+ import { isAbsolute, join, parse, resolve, sep } from "node:path";
3
+ import { FileEnumerator } from "../../shared/FileEnumerator";
4
4
  import { Paths } from "../../shared/Paths";
5
5
 
6
6
  export type FileCandidate = {
@@ -27,7 +27,6 @@ export type GitSpawner = (
27
27
 
28
28
  export type LoadRelativeOptions = {
29
29
  readonly root: string;
30
- readonly limit?: number;
31
30
  readonly gitSpawner?: GitSpawner;
32
31
  };
33
32
 
@@ -35,8 +34,6 @@ export type LoadAbsoluteOptions = {
35
34
  readonly query: string;
36
35
  };
37
36
 
38
- const DEFAULT_LIMIT = 10_000;
39
-
40
37
  const defaultGitSpawner: GitSpawner = async (args, options) => {
41
38
  try {
42
39
  const child = Bun.spawn(["git", ...args], {
@@ -126,16 +123,15 @@ async function runLoadRelative(
126
123
  root: string,
127
124
  options: LoadRelativeOptions
128
125
  ): Promise<readonly FileCandidate[]> {
129
- const limit = options.limit ?? DEFAULT_LIMIT;
130
126
  const spawner = options.gitSpawner ?? defaultGitSpawner;
131
127
 
132
128
  const fastPath = await tryGitListFiles(root, spawner);
133
129
  if (fastPath !== undefined) {
134
- return finalizeRelative(fastPath, limit);
130
+ return finalizeRelative(fastPath);
135
131
  }
136
132
 
137
133
  const fallback = await scanWithGlob(root);
138
- return finalizeRelative(fallback, limit);
134
+ return finalizeRelative(fallback);
139
135
  }
140
136
 
141
137
  async function tryGitListFiles(
@@ -155,39 +151,48 @@ async function tryGitListFiles(
155
151
  }
156
152
 
157
153
  async function scanWithGlob(root: string): Promise<readonly string[]> {
158
- const filter = await GitignoreFilter.for(root);
159
- const glob = new Bun.Glob("**/*");
160
- const matches: string[] = [];
161
-
162
- for await (const absolutePath of glob.scan({
163
- cwd: root,
164
- absolute: true,
165
- onlyFiles: true,
166
- dot: false,
167
- })) {
168
- if (!filter.ignores(absolutePath)) {
169
- matches.push(Paths.toForwardSlashes(relative(root, absolutePath)));
154
+ // FileEnumerator already returns ignore-respecting, root-relative POSIX file
155
+ // paths; directories are recovered from prefixes in finalizeRelative.
156
+ return FileEnumerator.enumerate(root, {
157
+ includeDotfiles: false,
158
+ includeIgnored: false,
159
+ });
160
+ }
161
+
162
+ function finalizeRelative(paths: readonly string[]): readonly FileCandidate[] {
163
+ const normalized = paths.map(Paths.toForwardSlashes);
164
+
165
+ // git ls-files only emits files; recover directories from their prefixes.
166
+ const directories = new Set<string>();
167
+ for (const path of normalized) {
168
+ for (
169
+ let slash = path.indexOf("/");
170
+ slash !== -1;
171
+ slash = path.indexOf("/", slash + 1)
172
+ ) {
173
+ directories.add(path.slice(0, slash));
170
174
  }
171
175
  }
172
176
 
173
- return matches;
174
- }
177
+ const candidates = [
178
+ ...[...directories].map((path) => toRelativeCandidate(path, true)),
179
+ ...normalized.map((path) => toRelativeCandidate(path, false)),
180
+ ];
181
+ candidates.sort((a, b) => a.insertPath.localeCompare(b.insertPath));
175
182
 
176
- function finalizeRelative(
177
- paths: readonly string[],
178
- limit: number
179
- ): readonly FileCandidate[] {
180
- const normalized = paths.map(Paths.toForwardSlashes);
181
- normalized.sort((a, b) => a.localeCompare(b));
183
+ return candidates;
184
+ }
182
185
 
183
- const truncated =
184
- normalized.length > limit ? normalized.slice(0, limit) : normalized;
185
- return truncated.map((path) => ({
186
+ function toRelativeCandidate(
187
+ path: string,
188
+ isDirectory: boolean
189
+ ): FileCandidate {
190
+ return {
186
191
  insertPath: path,
187
192
  displayPath: path,
188
193
  matchHaystack: path,
189
- isDirectory: false,
190
- }));
194
+ isDirectory,
195
+ };
191
196
  }
192
197
 
193
198
  async function findAnchor(absolutePath: string): Promise<string> {
@@ -0,0 +1,72 @@
1
+ import type { FileCandidate } from "./catalog";
2
+ import { loadRelative } from "./catalog";
3
+ import type {
4
+ FilePickerWorkerRequest,
5
+ FilePickerWorkerResponse,
6
+ } from "./filePickerWorkerMessages";
7
+ import { rank } from "./ranker";
8
+
9
+ let cachedRelative: readonly FileCandidate[] | undefined;
10
+
11
+ const toErrorMessage = (error: unknown): string => {
12
+ if (error instanceof Error) {
13
+ return error.message;
14
+ }
15
+ return String(error);
16
+ };
17
+
18
+ const refreshRelative = async (id: number, root: string): Promise<void> => {
19
+ try {
20
+ cachedRelative = await loadRelative({ root });
21
+ self.postMessage({
22
+ id,
23
+ type: "refreshRelative",
24
+ ok: true,
25
+ } satisfies FilePickerWorkerResponse);
26
+ } catch (error) {
27
+ if (cachedRelative === undefined) {
28
+ cachedRelative = [];
29
+ }
30
+ self.postMessage({
31
+ id,
32
+ type: "refreshRelative",
33
+ ok: false,
34
+ error: toErrorMessage(error),
35
+ } satisfies FilePickerWorkerResponse);
36
+ }
37
+ };
38
+
39
+ const rankSuggestions = async (
40
+ id: number,
41
+ query: string,
42
+ limit: number | undefined
43
+ ): Promise<void> => {
44
+ try {
45
+ const items = await rank(query, { cachedRelative, limit });
46
+ self.postMessage({
47
+ id,
48
+ type: "rank",
49
+ ok: true,
50
+ items,
51
+ } satisfies FilePickerWorkerResponse);
52
+ } catch (error) {
53
+ self.postMessage({
54
+ id,
55
+ type: "rank",
56
+ ok: false,
57
+ error: toErrorMessage(error),
58
+ } satisfies FilePickerWorkerResponse);
59
+ }
60
+ };
61
+
62
+ self.onmessage = (event: MessageEvent<FilePickerWorkerRequest>): void => {
63
+ const message = event.data;
64
+ switch (message.type) {
65
+ case "refreshRelative":
66
+ void refreshRelative(message.id, message.root);
67
+ break;
68
+ case "rank":
69
+ void rankSuggestions(message.id, message.query, message.limit);
70
+ break;
71
+ }
72
+ };
@@ -0,0 +1,39 @@
1
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
+
3
+ export type FilePickerWorkerRequest =
4
+ | {
5
+ readonly id: number;
6
+ readonly type: "refreshRelative";
7
+ readonly root: string;
8
+ }
9
+ | {
10
+ readonly id: number;
11
+ readonly type: "rank";
12
+ readonly query: string;
13
+ readonly limit?: number;
14
+ };
15
+
16
+ export type FilePickerWorkerResponse =
17
+ | {
18
+ readonly id: number;
19
+ readonly type: "refreshRelative";
20
+ readonly ok: true;
21
+ }
22
+ | {
23
+ readonly id: number;
24
+ readonly type: "refreshRelative";
25
+ readonly ok: false;
26
+ readonly error: string;
27
+ }
28
+ | {
29
+ readonly id: number;
30
+ readonly type: "rank";
31
+ readonly ok: true;
32
+ readonly items: readonly AutocompleteItem[] | undefined;
33
+ }
34
+ | {
35
+ readonly id: number;
36
+ readonly type: "rank";
37
+ readonly ok: false;
38
+ readonly error: string;
39
+ };