pim-agent 0.0.1 → 0.9.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -7
  3. package/bin/pim.ts +151 -0
  4. package/package.json +82 -4
  5. package/packages/core/src/attachments/AttachmentStore.ts +99 -0
  6. package/packages/core/src/attachments/Attachments.ts +69 -0
  7. package/packages/core/src/extensions/CoreExtensions.ts +51 -0
  8. package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
  9. package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
  10. package/packages/core/src/extensions/apply-patch/index.ts +69 -0
  11. package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
  12. package/packages/core/src/extensions/apply-patch/model.ts +39 -0
  13. package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
  14. package/packages/core/src/extensions/apply-patch/render.ts +216 -0
  15. package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
  16. package/packages/core/src/extensions/apply-patch/types.ts +30 -0
  17. package/packages/core/src/extensions/bash/capture.ts +86 -0
  18. package/packages/core/src/extensions/bash/format.ts +110 -0
  19. package/packages/core/src/extensions/bash/image.ts +31 -0
  20. package/packages/core/src/extensions/bash/index.ts +88 -0
  21. package/packages/core/src/extensions/bash/render.ts +31 -0
  22. package/packages/core/src/extensions/bash/run.ts +221 -0
  23. package/packages/core/src/extensions/bash/schema.ts +64 -0
  24. package/packages/core/src/extensions/edit/edit.ts +366 -0
  25. package/packages/core/src/extensions/edit/index.ts +36 -0
  26. package/packages/core/src/extensions/edit/render.ts +16 -0
  27. package/packages/core/src/extensions/edit/schema.ts +37 -0
  28. package/packages/core/src/extensions/glob/glob.ts +34 -0
  29. package/packages/core/src/extensions/glob/index.ts +83 -0
  30. package/packages/core/src/extensions/glob/render.ts +72 -0
  31. package/packages/core/src/extensions/glob/schema.ts +66 -0
  32. package/packages/core/src/extensions/grep/grep.ts +256 -0
  33. package/packages/core/src/extensions/grep/index.ts +114 -0
  34. package/packages/core/src/extensions/grep/render.ts +271 -0
  35. package/packages/core/src/extensions/grep/schema.ts +92 -0
  36. package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
  37. package/packages/core/src/extensions/read/image.ts +57 -0
  38. package/packages/core/src/extensions/read/index.ts +88 -0
  39. package/packages/core/src/extensions/read/read.ts +191 -0
  40. package/packages/core/src/extensions/read/render.ts +58 -0
  41. package/packages/core/src/extensions/read/schema.ts +49 -0
  42. package/packages/core/src/extensions/subagent/index.ts +33 -0
  43. package/packages/core/src/extensions/subagent/render.ts +127 -0
  44. package/packages/core/src/extensions/subagent/schema.ts +9 -0
  45. package/packages/core/src/extensions/subagent/subagent.ts +419 -0
  46. package/packages/core/src/extensions/system-prompt/index.ts +28 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
  48. package/packages/core/src/extensions/todo/index.ts +123 -0
  49. package/packages/core/src/extensions/todo/render.ts +156 -0
  50. package/packages/core/src/extensions/todo/schema.ts +24 -0
  51. package/packages/core/src/extensions/todo/todo.ts +185 -0
  52. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
  53. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
  54. package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
  55. package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
  56. package/packages/core/src/extensions/web-fetch/image.ts +165 -0
  57. package/packages/core/src/extensions/web-fetch/index.ts +83 -0
  58. package/packages/core/src/extensions/web-fetch/render.ts +100 -0
  59. package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
  60. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
  61. package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
  62. package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
  63. package/packages/core/src/extensions/web-search/index.ts +90 -0
  64. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
  65. package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
  66. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
  67. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
  68. package/packages/core/src/extensions/web-search/render.ts +49 -0
  69. package/packages/core/src/extensions/web-search/schema.ts +29 -0
  70. package/packages/core/src/extensions/web-search/search.ts +23 -0
  71. package/packages/core/src/extensions/write/index.ts +58 -0
  72. package/packages/core/src/extensions/write/render.ts +16 -0
  73. package/packages/core/src/extensions/write/schema.ts +14 -0
  74. package/packages/core/src/extensions/write/write.ts +87 -0
  75. package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
  76. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  77. package/packages/core/src/picker/PickerItem.ts +6 -0
  78. package/packages/core/src/picker/PickerService.ts +110 -0
  79. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
  80. package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  81. package/packages/core/src/picker/catalog.ts +220 -0
  82. package/packages/core/src/picker/commandRanker.ts +17 -0
  83. package/packages/core/src/picker/filePickerWorker.ts +66 -0
  84. package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
  85. package/packages/core/src/picker/ranker.ts +245 -0
  86. package/packages/core/src/picker/token.ts +70 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +213 -0
  89. package/packages/core/src/session/MessageText.ts +21 -0
  90. package/packages/core/src/session/ReadCursors.ts +94 -0
  91. package/packages/core/src/session/SessionCache.ts +68 -0
  92. package/packages/core/src/session/SessionHost.ts +876 -0
  93. package/packages/core/src/session/SessionLease.ts +279 -0
  94. package/packages/core/src/session/SessionRegistry.ts +169 -0
  95. package/packages/core/src/session/WriteMark.ts +37 -0
  96. package/packages/core/src/shared/Cli.ts +25 -0
  97. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  98. package/packages/core/src/shared/DiffLines.ts +304 -0
  99. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  100. package/packages/core/src/shared/DiffView.ts +29 -0
  101. package/packages/core/src/shared/Directories.ts +61 -0
  102. package/packages/core/src/shared/EditMatcher.ts +767 -0
  103. package/packages/core/src/shared/Errors.ts +9 -0
  104. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  105. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  106. package/packages/core/src/shared/FileScanner.ts +57 -0
  107. package/packages/core/src/shared/FileWatch.ts +107 -0
  108. package/packages/core/src/shared/Format.ts +74 -0
  109. package/packages/core/src/shared/Fs.ts +91 -0
  110. package/packages/core/src/shared/FsErrors.ts +67 -0
  111. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  112. package/packages/core/src/shared/Git.ts +89 -0
  113. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  114. package/packages/core/src/shared/Http.ts +14 -0
  115. package/packages/core/src/shared/ImageMime.ts +36 -0
  116. package/packages/core/src/shared/Images.ts +357 -0
  117. package/packages/core/src/shared/Json.ts +19 -0
  118. package/packages/core/src/shared/Languages.ts +95 -0
  119. package/packages/core/src/shared/Levenshtein.ts +30 -0
  120. package/packages/core/src/shared/Lines.ts +79 -0
  121. package/packages/core/src/shared/McpClient.ts +444 -0
  122. package/packages/core/src/shared/OutputBudget.ts +84 -0
  123. package/packages/core/src/shared/PatchSummary.ts +40 -0
  124. package/packages/core/src/shared/Paths.ts +80 -0
  125. package/packages/core/src/shared/PimSettings.ts +128 -0
  126. package/packages/core/src/shared/PimVersion.ts +93 -0
  127. package/packages/core/src/shared/Pool.ts +26 -0
  128. package/packages/core/src/shared/Proc.ts +30 -0
  129. package/packages/core/src/shared/RateLimiter.ts +50 -0
  130. package/packages/core/src/shared/Renderer.ts +371 -0
  131. package/packages/core/src/shared/SafePath.ts +17 -0
  132. package/packages/core/src/shared/SearchRender.ts +82 -0
  133. package/packages/core/src/shared/SendFile.ts +23 -0
  134. package/packages/core/src/shared/SpillCache.ts +78 -0
  135. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  136. package/packages/core/src/shared/Supervisor.ts +460 -0
  137. package/packages/core/src/shared/Sweeper.ts +32 -0
  138. package/packages/core/src/shared/Tools.ts +801 -0
  139. package/packages/core/src/shared/Updater.ts +193 -0
  140. package/packages/core/src/view/AnsiPainter.ts +227 -0
  141. package/packages/core/src/view/BodyRenderer.ts +102 -0
  142. package/packages/core/src/view/DiffBlocks.ts +52 -0
  143. package/packages/core/src/view/DiffLayout.ts +69 -0
  144. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  145. package/packages/core/src/view/Painting.ts +182 -0
  146. package/packages/core/src/view/ViewBlock.ts +124 -0
  147. package/packages/daemon/src/Daemon.ts +63 -0
  148. package/packages/daemon/src/DaemonInstall.ts +115 -0
  149. package/packages/daemon/src/Surfaces.ts +66 -0
  150. package/packages/daemon/src/TelegramSurface.ts +30 -0
  151. package/packages/daemon/src/WebSurface.ts +57 -0
  152. package/packages/daemon/src/index.ts +37 -0
  153. package/packages/protocol/src/Command.ts +92 -0
  154. package/packages/protocol/src/ImageRoute.ts +23 -0
  155. package/packages/protocol/src/Protocol.ts +6 -0
  156. package/packages/protocol/src/ServerEvent.ts +251 -0
  157. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  158. package/packages/server/src/ClientConnection.ts +265 -0
  159. package/packages/server/src/ImageEndpoint.ts +36 -0
  160. package/packages/server/src/ProbeClient.ts +312 -0
  161. package/packages/server/src/Reloader.ts +93 -0
  162. package/packages/server/src/SendFileTool.ts +88 -0
  163. package/packages/server/src/SessionCatalogue.ts +202 -0
  164. package/packages/server/src/SessionProjection.ts +194 -0
  165. package/packages/server/src/SessionStream.ts +601 -0
  166. package/packages/server/src/StaticClient.ts +95 -0
  167. package/packages/server/src/WebOptions.ts +72 -0
  168. package/packages/server/src/WsGateway.ts +563 -0
  169. package/packages/server/src/probe.ts +146 -0
  170. package/packages/telegram/src/Bot.ts +229 -0
  171. package/packages/telegram/src/Commands.ts +768 -0
  172. package/packages/telegram/src/Config.ts +148 -0
  173. package/packages/telegram/src/Markdown.ts +235 -0
  174. package/packages/telegram/src/Message.ts +152 -0
  175. package/packages/telegram/src/Renderer.ts +456 -0
  176. package/packages/telegram/src/SendFileSchema.ts +18 -0
  177. package/packages/telegram/src/SendFileTool.ts +65 -0
  178. package/packages/telegram/src/Session.ts +252 -0
  179. package/packages/telegram/src/SessionRegistry.ts +126 -0
  180. package/packages/telegram/src/TaskScheduler.ts +296 -0
  181. package/packages/telegram/src/TaskSchema.ts +88 -0
  182. package/packages/telegram/src/TaskStore.ts +67 -0
  183. package/packages/telegram/src/TaskTool.ts +208 -0
  184. package/packages/telegram/src/TelegramHtml.ts +288 -0
  185. package/packages/telegram/src/TypingIndicator.ts +43 -0
  186. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  187. package/packages/tui/src/extensions/_init/index.ts +144 -0
  188. package/packages/tui/src/extensions/command-picker/index.ts +43 -0
  189. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  190. package/packages/tui/src/extensions/footer/index.ts +117 -0
  191. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  192. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  193. package/packages/tui/src/extensions/pim/index.ts +89 -0
  194. package/packages/tui/src/extensions/session-lease/index.ts +248 -0
  195. package/packages/tui/src/extensions/tps/index.ts +118 -0
  196. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  197. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  198. package/packages/tui/src/themes/pim-dark.json +84 -0
  199. package/packages/tui/src/themes/pim-light.json +84 -0
  200. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  201. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  202. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  203. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  204. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  205. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  206. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  207. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  208. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  209. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  210. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  211. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  212. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  213. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  214. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  215. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  216. package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
  217. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  218. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  219. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  220. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  221. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  222. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  223. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  224. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  225. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  226. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  227. package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
  228. package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
  229. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  230. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  231. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  232. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  233. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  234. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  235. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  236. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  237. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  238. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  239. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  240. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  241. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  242. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  243. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  244. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  245. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  246. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  247. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  248. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  249. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  250. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  251. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  252. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  253. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  254. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  255. package/packages/web/dist/client/favicon.svg +1 -0
  256. package/packages/web/dist/client/icon-192.png +0 -0
  257. package/packages/web/dist/client/icon-512.png +0 -0
  258. package/packages/web/dist/client/icon-maskable.png +0 -0
  259. package/packages/web/dist/client/index.html +24 -0
  260. package/packages/web/dist/client/manifest.webmanifest +21 -0
  261. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,14 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const writeSchema = Type.Object({
4
+ path: Type.String({
5
+ description:
6
+ "Absolute or relative path to file (resolved against cwd). Parent directories are created if missing.",
7
+ }),
8
+ content: Type.String({
9
+ description:
10
+ "UTF-8 text written verbatim. Include a trailing newline if needed.",
11
+ }),
12
+ });
13
+
14
+ export type WriteInput = Static<typeof writeSchema>;
@@ -0,0 +1,87 @@
1
+ import { DiffLines, type ToolDiff } from "../../shared/DiffLines";
2
+ import { Fs } from "../../shared/Fs";
3
+ import { FsErrors } from "../../shared/FsErrors";
4
+
5
+ const CONTEXT_LINES = 3;
6
+
7
+ // Diff.diffLines is O(n*d): above this only the diff is skipped, never the write.
8
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
9
+
10
+ export type WriteOutcome = {
11
+ readonly bytesWritten: number;
12
+ readonly created: boolean;
13
+ readonly diff?: ToolDiff;
14
+ readonly diffSkipped?: {
15
+ readonly reason: "size";
16
+ readonly thresholdBytes: number;
17
+ readonly comparedBytes: number;
18
+ };
19
+ readonly trailingNewlineChange?: "added" | "removed";
20
+ };
21
+
22
+ export async function writeContent(
23
+ absolutePath: string,
24
+ content: string
25
+ ): Promise<WriteOutcome> {
26
+ const prior = await readPriorContent(absolutePath);
27
+ const bytesWritten = Buffer.byteLength(content, "utf8");
28
+
29
+ if (prior === content) {
30
+ return { bytesWritten, created: false };
31
+ }
32
+
33
+ await Fs.writeAtomic(absolutePath, content);
34
+
35
+ const created = prior === undefined;
36
+ const priorBytes = prior === undefined ? 0 : Buffer.byteLength(prior, "utf8");
37
+ const comparedBytes = Math.max(priorBytes, bytesWritten);
38
+ const priorNewline = prior?.endsWith("\n") ?? false;
39
+ const contentNewline = content.endsWith("\n");
40
+ const trailingNewlineChange =
41
+ created || priorNewline === contentNewline
42
+ ? undefined
43
+ : contentNewline
44
+ ? "added"
45
+ : "removed";
46
+
47
+ if (comparedBytes > MAX_DIFF_BYTES) {
48
+ return {
49
+ bytesWritten,
50
+ created,
51
+ diffSkipped: {
52
+ reason: "size",
53
+ thresholdBytes: MAX_DIFF_BYTES,
54
+ comparedBytes,
55
+ },
56
+ ...(trailingNewlineChange === undefined ? {} : { trailingNewlineChange }),
57
+ };
58
+ }
59
+
60
+ const diff = DiffLines.buildToolDiff(
61
+ absolutePath,
62
+ prior === undefined ? DiffLines.emptySide : DiffLines.fromText(prior),
63
+ DiffLines.fromText(content),
64
+ CONTEXT_LINES
65
+ );
66
+
67
+ return {
68
+ bytesWritten,
69
+ created,
70
+ ...(diff === undefined ? {} : { diff }),
71
+ ...(trailingNewlineChange === undefined ? {} : { trailingNewlineChange }),
72
+ };
73
+ }
74
+
75
+ async function readPriorContent(
76
+ absolutePath: string
77
+ ): Promise<string | undefined> {
78
+ try {
79
+ return await Bun.file(absolutePath).text();
80
+ } catch (error) {
81
+ if (FsErrors.code(error) === "ENOENT") {
82
+ return undefined;
83
+ }
84
+
85
+ throw error;
86
+ }
87
+ }
@@ -0,0 +1,14 @@
1
+ import type { PickerItem } from "./PickerItem";
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 PickerItem[] | undefined>;
14
+ };
@@ -0,0 +1,52 @@
1
+ import type { PickerItem } from "./PickerItem";
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 PickerItem[] | 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,6 @@
1
+ /** One completion row, structurally identical to pi-tui's `AutocompleteItem`. */
2
+ export type PickerItem = {
3
+ readonly value: string;
4
+ readonly label: string;
5
+ readonly description?: string;
6
+ };
@@ -0,0 +1,110 @@
1
+ import { type AgentSession, loadSkills } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { loadRelative } from "./catalog";
4
+ import { rankCommands } from "./commandRanker";
5
+ import { InProcessFilePickerSuggestionEngine } from "./InProcessFilePickerSuggestionEngine";
6
+ import type { PickerItem } from "./PickerItem";
7
+
8
+ export type PickerServiceDeps = {
9
+ /** Read per query: a session's cwd can move between turns. */
10
+ readonly cwd: () => string;
11
+ readonly agentDir: string;
12
+ /** Extension-registered commands, available only while an agent is built. */
13
+ readonly agent?: () => AgentSession | undefined;
14
+ };
15
+
16
+ const DEFAULT_LIMIT = 50;
17
+
18
+ /** Answers one session's picker queries against the machine the agent runs on. */
19
+ export class PickerService {
20
+ private readonly deps: PickerServiceDeps;
21
+ private fileCache:
22
+ | {
23
+ readonly cwd: string;
24
+ readonly engine: InProcessFilePickerSuggestionEngine;
25
+ loaded: boolean;
26
+ }
27
+ | undefined;
28
+ private commandCache:
29
+ | { readonly cwd: string; readonly items: readonly PickerItem[] }
30
+ | undefined;
31
+
32
+ public constructor(deps: PickerServiceDeps) {
33
+ this.deps = deps;
34
+ }
35
+
36
+ public async files(
37
+ query: string,
38
+ limit = DEFAULT_LIMIT
39
+ ): Promise<readonly PickerItem[]> {
40
+ const cache = this.fileEngine();
41
+ if (!cache.loaded) {
42
+ await cache.engine.refreshRelative();
43
+ cache.loaded = true;
44
+ }
45
+ return (await cache.engine.rank(query, { limit })) ?? [];
46
+ }
47
+
48
+ public commands(query: string, limit = DEFAULT_LIMIT): readonly PickerItem[] {
49
+ return rankCommands(query, this.commandItems(), { limit });
50
+ }
51
+
52
+ public invalidate(): void {
53
+ this.fileCache = undefined;
54
+ this.commandCache = undefined;
55
+ }
56
+
57
+ private fileEngine(): NonNullable<PickerService["fileCache"]> {
58
+ const cwd = this.deps.cwd();
59
+ if (this.fileCache?.cwd !== cwd) {
60
+ this.fileCache = {
61
+ cwd,
62
+ engine: new InProcessFilePickerSuggestionEngine({
63
+ loadRelativeCatalog: () => loadRelative({ root: cwd }),
64
+ }),
65
+ loaded: false,
66
+ };
67
+ }
68
+ return this.fileCache;
69
+ }
70
+
71
+ private commandItems(): readonly PickerItem[] {
72
+ const cwd = this.deps.cwd();
73
+ if (this.commandCache?.cwd !== cwd) {
74
+ this.commandCache = { cwd, items: this.loadSkillItems(cwd) };
75
+ }
76
+ return [...this.commandCache.items, ...this.extensionItems()];
77
+ }
78
+
79
+ private loadSkillItems(cwd: string): readonly PickerItem[] {
80
+ try {
81
+ const { skills } = loadSkills({
82
+ cwd,
83
+ agentDir: this.deps.agentDir,
84
+ skillPaths: [],
85
+ includeDefaults: true,
86
+ });
87
+ return skills.map((skill) => ({
88
+ value: `/skill:${skill.name}`,
89
+ label: `/skill:${skill.name}`,
90
+ description: skill.description,
91
+ }));
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ private extensionItems(): readonly PickerItem[] {
98
+ const agent = this.deps.agent?.();
99
+ if (!agent) {
100
+ return [];
101
+ }
102
+ return agent.extensionRunner.getRegisteredCommands().map((command) => ({
103
+ value: `/${command.invocationName}`,
104
+ label: `/${command.invocationName}`,
105
+ ...(command.description === undefined
106
+ ? {}
107
+ : { description: command.description }),
108
+ }));
109
+ }
110
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ FilePickerSuggestionEngine,
3
+ RankFilePickerOptions,
4
+ } from "./FilePickerSuggestionEngine";
5
+ import type { PickerItem } from "./PickerItem";
6
+
7
+ export type RemotePickerQuery = (
8
+ query: string,
9
+ limit: number | undefined
10
+ ) => Promise<readonly PickerItem[]>;
11
+
12
+ const DEBOUNCE_MS = 30;
13
+
14
+ const CACHE_LIMIT = 100;
15
+
16
+ /** The client half of the `@` picker: the TUI's engine contract, answered by the server. Browser-safe APIs only. */
17
+ export class RemoteFilePickerSuggestionEngine implements FilePickerSuggestionEngine {
18
+ private readonly cache = new Map<string, readonly PickerItem[]>();
19
+ private generation = 0;
20
+
21
+ public constructor(
22
+ private readonly query: RemotePickerQuery,
23
+ private readonly debounceMs: number = DEBOUNCE_MS
24
+ ) {}
25
+
26
+ /** The invalidation hook: `picker_invalidate` from the server lands here. */
27
+ public refreshRelative(): Promise<void> {
28
+ this.cache.clear();
29
+ return Promise.resolve();
30
+ }
31
+
32
+ public async rank(
33
+ query: string,
34
+ options: RankFilePickerOptions
35
+ ): Promise<readonly PickerItem[] | undefined> {
36
+ const key = `${options.limit ?? ""}\u0000${query}`;
37
+ const cached = this.cache.get(key);
38
+ if (cached) {
39
+ return cached;
40
+ }
41
+ const mine = ++this.generation;
42
+ await new Promise((resolve) => setTimeout(resolve, this.debounceMs));
43
+ if (mine !== this.generation || options.signal?.aborted === true) {
44
+ return [];
45
+ }
46
+ const items = await this.query(query, options.limit);
47
+ this.cache.set(key, items);
48
+ if (this.cache.size > CACHE_LIMIT) {
49
+ const oldest = this.cache.keys().next().value;
50
+ if (oldest !== undefined) {
51
+ this.cache.delete(oldest);
52
+ }
53
+ }
54
+ return mine === this.generation ? items : [];
55
+ }
56
+ }
@@ -0,0 +1,268 @@
1
+ import type { PickerItem } from "./PickerItem";
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 PickerItem[] | 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 PickerItem[] | 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 PickerItem[] | 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
+ }