@aaroncql/pim-agent 0.2.0 โ†’ 0.4.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.
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  <!-- omit in toc -->
2
2
  # PIM - Pi IMproved
3
3
 
4
+ [![npm version](https://img.shields.io/npm/v/@aaroncql/pim-agent?style=flat-square)](https://www.npmjs.com/package/@aaroncql/pim-agent)
5
+ [![npm downloads](https://img.shields.io/npm/dm/@aaroncql/pim-agent?style=flat-square)](https://www.npmjs.com/package/@aaroncql/pim-agent)
6
+ [![license](https://img.shields.io/npm/l/@aaroncql/pim-agent?style=flat-square)](./LICENSE)
7
+ [![Bun](https://img.shields.io/badge/runtime-Bun-black?logo=bun&style=flat-square)](https://bun.com)
8
+
4
9
  _**Pim is to Pi what Vim is to Vi.**_
5
10
 
6
11
  A Bun-native extension pack for [Pi](https://pi.dev/): web access, subagents, revamped core tools, ANSI-compatible themes, fzf-style completions, Telegram mode, and more. Preliminary score of [37.8% on Terminal-Bench 2.0](#terminal-bench-20) with locally hosted Qwen3.6-35B, rivalling Claude Code + Sonnet 4.5.
@@ -19,6 +24,7 @@ A Bun-native extension pack for [Pi](https://pi.dev/): web access, subagents, re
19
24
  - [Setup](#setup)
20
25
  - [Commands](#commands)
21
26
  - [Features](#features)
27
+ - [Changelog](#changelog)
22
28
  - [Developing](#developing)
23
29
 
24
30
  ![Pim Demo](https://raw.githubusercontent.com/AaronCQL/pim-agent/refs/heads/main/assets/demo.webp)
@@ -226,10 +232,14 @@ For development, run standalone with `pim --mode telegram` instead.
226
232
 
227
233
  - โฐ **Scheduled tasks** - your bot can create one-time, interval, or cron-based tasks that fire automatically; ask your bot to schedule something.
228
234
  - ๐Ÿ‘€ **Live progress logs** - use `/logs` to choose what you see while the agent works: final replies, tool use, intermediate text, or thinking.
229
- - ๐Ÿ“ **Markdown formatting** - replies render Markdown out of the box, including tables converted to vertical lists for Telegram.
235
+ - ๐Ÿ“ **Rich Markdown** - supports Telegram's [rich text formatting](https://telegram.org/blog/watch-apps-and-more#obscenely-rich-text-formatting-for-bots) with full markdown and LaTeX math support.
230
236
  - ๐Ÿ“Ž **Rich media** - send photos, documents, videos, audio, and voice messages directly in chat; your bot can also send files back to you.
231
237
  - ๐Ÿงต **Thread-specific prompts** - each chat (or thread) gets its own session and optional instructions; ask your bot to modify its instructions.
232
238
 
239
+ ## Changelog
240
+
241
+ See [CHANGELOG.md](./CHANGELOG.md) for release notes.
242
+
233
243
  ## Developing
234
244
 
235
245
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaroncql/pim-agent",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A Bun-native extension pack for Pi: web access, subagents, revamped core tools, ANSI-compatible themes, fzf-style completions, Telegram mode, and more.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,16 +48,16 @@
48
48
  "@earendil-works/pi-coding-agent": "*"
49
49
  },
50
50
  "devDependencies": {
51
- "@earendil-works/pi-coding-agent": "0.76.0",
51
+ "@earendil-works/pi-coding-agent": "0.79.2",
52
52
  "@types/bun": "latest",
53
- "@typescript/native-preview": "^7.0.0-dev.20260505.1",
54
- "oxlint": "^1.62.0",
55
- "prettier": "^3.8.3"
53
+ "@typescript/native-preview": "^7.0.0-dev.20260612.1",
54
+ "oxlint": "^1.69.0",
55
+ "prettier": "^3.8.4"
56
56
  },
57
57
  "dependencies": {
58
58
  "diff": "^9.0.0",
59
59
  "fzf": "^0.5.2",
60
- "grammy": "^1.42.0",
60
+ "grammy": "^1.44.0",
61
61
  "ignore": "^7.0.5",
62
62
  "ky": "^2.0.2"
63
63
  }
@@ -2,28 +2,46 @@ const EDIT_TOOL = "edit";
2
2
  const APPLY_PATCH_TOOL = "apply_patch";
3
3
 
4
4
  /**
5
- * Pure reconcile over the active-tool list. Single-slot swap:
6
- * - If neither `edit` nor `apply_patch` is active, no-op (respect user opt-out).
7
- * - Otherwise keep exactly one in the same position: `apply_patch` when the
8
- * model is GPT/Codex-family, else `edit`. All other active tools are
9
- * preserved in order.
5
+ * Pure reconcile over available and active tool lists. Single-slot swap:
6
+ * - If exactly one of `edit` or `apply_patch` is available, use that tool.
7
+ * - If both are available, keep exactly one in the same position: `apply_patch`
8
+ * when the model is GPT/Codex-family, else `edit`.
9
+ * - If neither is active and availability does not force a choice, no-op
10
+ * (respect user opt-out). All other active tools are preserved in order.
10
11
  *
11
12
  * Returns the same array reference when nothing changes so callers can skip the
12
13
  * prompt-rebuilding `setActiveTools` call.
13
14
  */
14
15
  export function computeActiveTools(
16
+ available: readonly string[],
15
17
  active: readonly string[],
16
18
  isGpt: boolean
17
19
  ): readonly string[] {
18
20
  const hasEdit = active.includes(EDIT_TOOL);
19
21
  const hasApplyPatch = active.includes(APPLY_PATCH_TOOL);
22
+ const canEdit = available.includes(EDIT_TOOL);
23
+ const canApplyPatch = available.includes(APPLY_PATCH_TOOL);
20
24
 
21
25
  if (!hasEdit && !hasApplyPatch) {
26
+ if (canEdit !== canApplyPatch) {
27
+ return [...active, canEdit ? EDIT_TOOL : APPLY_PATCH_TOOL];
28
+ }
29
+ return active;
30
+ }
31
+
32
+ if (!canEdit && !canApplyPatch) {
22
33
  return active;
23
34
  }
24
35
 
25
- const desired = isGpt ? APPLY_PATCH_TOOL : EDIT_TOOL;
26
- const drop = isGpt ? EDIT_TOOL : APPLY_PATCH_TOOL;
36
+ const desired =
37
+ canEdit && !canApplyPatch
38
+ ? EDIT_TOOL
39
+ : canApplyPatch && !canEdit
40
+ ? APPLY_PATCH_TOOL
41
+ : isGpt
42
+ ? APPLY_PATCH_TOOL
43
+ : EDIT_TOOL;
44
+ const drop = desired === EDIT_TOOL ? APPLY_PATCH_TOOL : EDIT_TOOL;
27
45
 
28
46
  if (active.includes(desired) && !active.includes(drop)) {
29
47
  return active;
@@ -58,7 +58,8 @@ export default function (pi: ExtensionAPI): void {
58
58
 
59
59
  const reconcile = (isGpt: boolean): void => {
60
60
  const active = pi.getActiveTools();
61
- const next = computeActiveTools(active, isGpt);
61
+ const available = pi.getAllTools().map((tool) => tool.name);
62
+ const next = computeActiveTools(available, active, isGpt);
62
63
  if (next !== active) {
63
64
  pi.setActiveTools([...next]);
64
65
  }
@@ -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
+ };