pim-agent 0.0.1 → 0.10.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 (275) 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 +54 -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 +177 -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 +36 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +232 -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 +132 -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 +77 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +156 -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/SearchIndex.ts +752 -0
  92. package/packages/core/src/session/SearchTokens.ts +97 -0
  93. package/packages/core/src/session/SessionCache.ts +68 -0
  94. package/packages/core/src/session/SessionDigest.ts +161 -0
  95. package/packages/core/src/session/SessionHost.ts +892 -0
  96. package/packages/core/src/session/SessionLease.ts +279 -0
  97. package/packages/core/src/session/SessionMeta.ts +267 -0
  98. package/packages/core/src/session/SessionName.ts +18 -0
  99. package/packages/core/src/session/SessionRegistry.ts +244 -0
  100. package/packages/core/src/session/WriteMark.ts +51 -0
  101. package/packages/core/src/shared/Cli.ts +25 -0
  102. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  103. package/packages/core/src/shared/DiffLines.ts +304 -0
  104. package/packages/core/src/shared/DiffPatch.ts +70 -0
  105. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  106. package/packages/core/src/shared/DiffView.ts +29 -0
  107. package/packages/core/src/shared/Directories.ts +61 -0
  108. package/packages/core/src/shared/EditMatcher.ts +767 -0
  109. package/packages/core/src/shared/Errors.ts +9 -0
  110. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  111. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  112. package/packages/core/src/shared/FileScanner.ts +57 -0
  113. package/packages/core/src/shared/FileWatch.ts +121 -0
  114. package/packages/core/src/shared/Format.ts +74 -0
  115. package/packages/core/src/shared/Fs.ts +91 -0
  116. package/packages/core/src/shared/FsErrors.ts +67 -0
  117. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  118. package/packages/core/src/shared/Git.ts +530 -0
  119. package/packages/core/src/shared/GitMonitor.ts +221 -0
  120. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  121. package/packages/core/src/shared/Http.ts +14 -0
  122. package/packages/core/src/shared/ImageMime.ts +36 -0
  123. package/packages/core/src/shared/Images.ts +357 -0
  124. package/packages/core/src/shared/Json.ts +19 -0
  125. package/packages/core/src/shared/Languages.ts +95 -0
  126. package/packages/core/src/shared/Levenshtein.ts +83 -0
  127. package/packages/core/src/shared/Lines.ts +79 -0
  128. package/packages/core/src/shared/McpClient.ts +444 -0
  129. package/packages/core/src/shared/OutputBudget.ts +84 -0
  130. package/packages/core/src/shared/PatchSummary.ts +40 -0
  131. package/packages/core/src/shared/Paths.ts +80 -0
  132. package/packages/core/src/shared/PimSettings.ts +128 -0
  133. package/packages/core/src/shared/PimVersion.ts +93 -0
  134. package/packages/core/src/shared/Pool.ts +26 -0
  135. package/packages/core/src/shared/Proc.ts +50 -0
  136. package/packages/core/src/shared/RateLimiter.ts +50 -0
  137. package/packages/core/src/shared/Renderer.ts +371 -0
  138. package/packages/core/src/shared/RepoDiff.ts +741 -0
  139. package/packages/core/src/shared/SafePath.ts +17 -0
  140. package/packages/core/src/shared/SearchRender.ts +82 -0
  141. package/packages/core/src/shared/SendFile.ts +23 -0
  142. package/packages/core/src/shared/SpillCache.ts +78 -0
  143. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  144. package/packages/core/src/shared/Supervisor.ts +460 -0
  145. package/packages/core/src/shared/Surface.ts +2 -0
  146. package/packages/core/src/shared/Sweeper.ts +32 -0
  147. package/packages/core/src/shared/Tools.ts +801 -0
  148. package/packages/core/src/shared/Updater.ts +209 -0
  149. package/packages/core/src/view/AnsiPainter.ts +227 -0
  150. package/packages/core/src/view/BodyRenderer.ts +102 -0
  151. package/packages/core/src/view/DiffBlocks.ts +52 -0
  152. package/packages/core/src/view/DiffExpand.ts +214 -0
  153. package/packages/core/src/view/DiffLayout.ts +69 -0
  154. package/packages/core/src/view/DiffPairs.ts +47 -0
  155. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  156. package/packages/core/src/view/MovePath.ts +70 -0
  157. package/packages/core/src/view/Painting.ts +182 -0
  158. package/packages/core/src/view/ViewBlock.ts +124 -0
  159. package/packages/daemon/src/Daemon.ts +63 -0
  160. package/packages/daemon/src/DaemonInstall.ts +115 -0
  161. package/packages/daemon/src/Surfaces.ts +66 -0
  162. package/packages/daemon/src/TelegramSurface.ts +30 -0
  163. package/packages/daemon/src/WebSurface.ts +56 -0
  164. package/packages/daemon/src/index.ts +37 -0
  165. package/packages/protocol/src/Command.ts +219 -0
  166. package/packages/protocol/src/Diff.ts +15 -0
  167. package/packages/protocol/src/ImageRoute.ts +23 -0
  168. package/packages/protocol/src/ServerEvent.ts +406 -0
  169. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  170. package/packages/server/src/ClientConnection.ts +277 -0
  171. package/packages/server/src/ImageEndpoint.ts +36 -0
  172. package/packages/server/src/ProbeClient.ts +374 -0
  173. package/packages/server/src/Reloader.ts +93 -0
  174. package/packages/server/src/SendFileTool.ts +88 -0
  175. package/packages/server/src/SessionCatalogue.ts +456 -0
  176. package/packages/server/src/SessionProjection.ts +194 -0
  177. package/packages/server/src/SessionStream.ts +627 -0
  178. package/packages/server/src/StaticClient.ts +178 -0
  179. package/packages/server/src/WebOptions.ts +72 -0
  180. package/packages/server/src/WsGateway.ts +798 -0
  181. package/packages/server/src/probe.ts +200 -0
  182. package/packages/telegram/src/Bot.ts +229 -0
  183. package/packages/telegram/src/Commands.ts +768 -0
  184. package/packages/telegram/src/Config.ts +148 -0
  185. package/packages/telegram/src/Markdown.ts +235 -0
  186. package/packages/telegram/src/Message.ts +152 -0
  187. package/packages/telegram/src/Renderer.ts +456 -0
  188. package/packages/telegram/src/SendFileSchema.ts +18 -0
  189. package/packages/telegram/src/SendFileTool.ts +65 -0
  190. package/packages/telegram/src/Session.ts +253 -0
  191. package/packages/telegram/src/SessionRegistry.ts +126 -0
  192. package/packages/telegram/src/TaskScheduler.ts +296 -0
  193. package/packages/telegram/src/TaskSchema.ts +88 -0
  194. package/packages/telegram/src/TaskStore.ts +67 -0
  195. package/packages/telegram/src/TaskTool.ts +208 -0
  196. package/packages/telegram/src/TelegramHtml.ts +288 -0
  197. package/packages/telegram/src/TypingIndicator.ts +43 -0
  198. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  199. package/packages/tui/src/extensions/_init/index.ts +144 -0
  200. package/packages/tui/src/extensions/command-picker/index.ts +75 -0
  201. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  202. package/packages/tui/src/extensions/footer/index.ts +93 -0
  203. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  204. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  205. package/packages/tui/src/extensions/pim/index.ts +89 -0
  206. package/packages/tui/src/extensions/session-lease/index.ts +253 -0
  207. package/packages/tui/src/extensions/tps/index.ts +118 -0
  208. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  209. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  210. package/packages/tui/src/themes/pim-dark.json +84 -0
  211. package/packages/tui/src/themes/pim-light.json +84 -0
  212. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  213. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  214. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  215. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  216. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  217. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  218. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  219. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  220. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
  221. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
  222. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  223. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  224. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  225. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  226. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  227. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  228. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  229. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  230. package/packages/web/dist/client/assets/core-CA0aSzPu.js +3 -0
  231. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  232. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  233. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  234. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  235. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  236. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  237. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  238. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  239. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  240. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  241. package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
  242. package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
  243. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  244. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  245. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  246. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  247. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  248. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  249. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  250. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  251. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  252. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  253. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  254. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  255. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  256. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  257. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  258. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  259. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  260. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  261. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  262. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  263. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  264. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  265. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  266. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  267. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  268. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  269. package/packages/web/dist/client/favicon.svg +1 -0
  270. package/packages/web/dist/client/icon-192.png +0 -0
  271. package/packages/web/dist/client/icon-512.png +0 -0
  272. package/packages/web/dist/client/icon-maskable.png +0 -0
  273. package/packages/web/dist/client/index.html +24 -0
  274. package/packages/web/dist/client/manifest.webmanifest +21 -0
  275. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,627 @@
1
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { PickerService } from "#core/picker/PickerService";
4
+ import { MessageText } from "#core/session/MessageText";
5
+ import type { LeaseState, SessionHost } from "#core/session/SessionHost";
6
+ import { SessionLease, type LeaseRecord } from "#core/session/SessionLease";
7
+ import { FileWatch } from "#core/shared/FileWatch";
8
+ import { GitMonitor } from "#core/shared/GitMonitor";
9
+ import { Tools } from "#core/shared/Tools";
10
+ import type { ToolView } from "#core/view/ViewBlock";
11
+ import type {
12
+ EphemeralEvent,
13
+ ServerEvent,
14
+ StreamEvent,
15
+ } from "#protocol/ServerEvent";
16
+ import { SessionProjection } from "./SessionProjection";
17
+
18
+ export type StreamListener = (event: ServerEvent) => void;
19
+
20
+ type LiveTool = {
21
+ readonly callId: string;
22
+ readonly name: string;
23
+ readonly args: unknown;
24
+ view: ToolView;
25
+ isError: boolean;
26
+ done: boolean;
27
+ };
28
+
29
+ type LiveMessage = {
30
+ readonly messageId: string;
31
+ text: string;
32
+ thinking: string;
33
+ ended: boolean;
34
+ retired: boolean;
35
+ readonly tools: LiveTool[];
36
+ };
37
+
38
+ export type SessionStreamDeps = {
39
+ /** How often the file watch falls back to a poll; the default is a second. */
40
+ readonly pollMs?: number;
41
+ /** Shared with every other session in the same directory; one is made here when none is given. */
42
+ readonly git?: GitMonitor;
43
+ /**
44
+ * Whether a session in this stream's directory is mid-turn, this one
45
+ * included. Only the gateway can see the others, so a stream standing alone
46
+ * reports nothing rather than guessing from itself.
47
+ */
48
+ readonly repoBusy?: () => boolean;
49
+ };
50
+
51
+ function sameLease(a: LeaseState, b: LeaseState): boolean {
52
+ return (
53
+ a.writable === b.writable &&
54
+ a.heldBy?.pid === b.heldBy?.pid &&
55
+ a.heldBy?.frontend === b.heldBy?.frontend
56
+ );
57
+ }
58
+
59
+ /** One session's view of the world, shared by every client attached to it and kept running when none are. */
60
+ export class SessionStream {
61
+ public readonly sessionId: string;
62
+ public readonly host: SessionHost;
63
+ public readonly picker: PickerService;
64
+ private readonly sessionPath: string;
65
+ private readonly projection: SessionProjection;
66
+ private readonly listeners = new Set<StreamListener>();
67
+ private readonly pollMs: number | undefined;
68
+ private readonly git: GitMonitor;
69
+ private readonly repoBusy: (() => boolean) | undefined;
70
+ private liveTurn: LiveMessage[] = [];
71
+ private unsubscribe: (() => void) | undefined;
72
+ private unsubscribeLease: (() => void) | undefined;
73
+ private unsubscribeForeign: (() => void) | undefined;
74
+ private unwatch: (() => void) | undefined;
75
+ private holder: LeaseRecord | undefined;
76
+ private lease: LeaseState = { writable: true };
77
+ private draining: Promise<void> = Promise.resolve();
78
+ private drainQueued = false;
79
+ private sentSeq = 0;
80
+ private liveMessageId = 0;
81
+ private turnStartedAt = 0;
82
+ private gitCwd: string | undefined;
83
+ private gitStop: (() => void) | undefined;
84
+ private gitBranch: string | null = null;
85
+
86
+ public constructor(
87
+ sessionId: string,
88
+ host: SessionHost,
89
+ sessionPath: string,
90
+ deps: SessionStreamDeps = {}
91
+ ) {
92
+ this.sessionId = sessionId;
93
+ this.host = host;
94
+ this.sessionPath = sessionPath;
95
+ this.pollMs = deps.pollMs;
96
+ this.git = deps.git ?? new GitMonitor();
97
+ this.repoBusy = deps.repoBusy;
98
+ this.projection = new SessionProjection(sessionPath, () => host.cwd);
99
+ this.picker = new PickerService({
100
+ cwd: () => host.cwd,
101
+ agentDir: host.agentDir,
102
+ agent: () => host.agentSession,
103
+ });
104
+ }
105
+
106
+ /** Follow the host, not one agent: a rehydrated session is a new `AgentSession`. */
107
+ public start(): void {
108
+ this.unsubscribe ??= this.host.subscribe((event) => {
109
+ this.onAgentEvent(event);
110
+ });
111
+ this.unsubscribeLease ??= this.host.onLeaseChange(() => {
112
+ void this.pushLease();
113
+ });
114
+ this.unsubscribeForeign ??= this.host.onForeignWrite(() => {
115
+ this.push({
116
+ type: "error",
117
+ message:
118
+ "Another process is writing this session; its history may be inconsistent.",
119
+ });
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Watch the lease and the session file while a client is reading: un-greying
125
+ * has to feel instant, and a turn another process runs reaches the browser
126
+ * only through the file, which emits no agent event here. Idle sessions pay
127
+ * nothing.
128
+ */
129
+ public watchFiles(active: boolean): void {
130
+ if (active === (this.unwatch !== undefined)) {
131
+ return;
132
+ }
133
+ if (!active) {
134
+ this.unwatch?.();
135
+ this.unwatch = undefined;
136
+ this.syncGit();
137
+ return;
138
+ }
139
+ const stops = [
140
+ SessionLease.watch(this.sessionPath, () => {
141
+ void this.pushLease();
142
+ }),
143
+ FileWatch.file(
144
+ this.sessionPath,
145
+ () => {
146
+ this.scheduleDrain();
147
+ },
148
+ this.pollMs
149
+ ),
150
+ ];
151
+ this.unwatch = (): void => {
152
+ for (const stop of stops) {
153
+ stop();
154
+ }
155
+ };
156
+ this.syncGit();
157
+ }
158
+
159
+ /** Re-points the git watch after the session moves; the monitor is shared, so a repeat is free. */
160
+ public syncGit(): void {
161
+ const wanted = this.unwatch === undefined ? undefined : this.host.cwd;
162
+ if (wanted === this.gitCwd) {
163
+ return;
164
+ }
165
+ this.gitStop?.();
166
+ this.gitStop = undefined;
167
+ this.gitCwd = wanted;
168
+ if (wanted === undefined) {
169
+ return;
170
+ }
171
+ this.gitStop = this.git.watch(wanted, (state) => {
172
+ // Whoever moved it — this client, the terminal, another window — every
173
+ // path the pickers hold is from the branch that just left. The first
174
+ // reading is a discovery rather than a move, and drops nothing.
175
+ const moved = this.gitBranch !== null && state.branch !== this.gitBranch;
176
+ this.gitBranch = state.branch;
177
+ if (moved) {
178
+ this.invalidatePickers("files");
179
+ }
180
+ this.emit(this.sessionState());
181
+ });
182
+ }
183
+
184
+ /** Reads the repository again for a client that has reason to think its picture is old. */
185
+ public async refreshGit(fetch: boolean): Promise<void> {
186
+ await this.git.refresh(this.host.cwd, { fetch });
187
+ }
188
+
189
+ /** Project whatever pi has appended since the last read, telling every client; returns the head. */
190
+ public async refresh(): Promise<number> {
191
+ await this.flushDurable();
192
+ return this.projection.head;
193
+ }
194
+
195
+ public subscribe(listener: StreamListener): () => void {
196
+ this.listeners.add(listener);
197
+ return () => {
198
+ this.listeners.delete(listener);
199
+ };
200
+ }
201
+
202
+ /** Whether `callId` names a tool this session is still running, log written or not. */
203
+ public isRunning(callId: string): boolean {
204
+ return this.findTool(callId)?.done === false;
205
+ }
206
+
207
+ /** Everything a client at `fromSeq` has not seen: the durable tail, the in-flight turn coalesced, then state. */
208
+ public async replay(fromSeq: number): Promise<readonly StreamEvent[]> {
209
+ await Promise.all([this.flushDurable(), this.readLease()]);
210
+ return [...this.projection.since(fromSeq), ...this.inFlight()];
211
+ }
212
+
213
+ private inFlight(): readonly StreamEvent[] {
214
+ const events: StreamEvent[] = [];
215
+ for (const message of this.liveTurn) {
216
+ const { messageId } = message;
217
+ events.push({ type: "message_start", role: "assistant", messageId });
218
+ if (message.thinking) {
219
+ events.push({
220
+ type: "thinking_delta",
221
+ messageId,
222
+ delta: message.thinking,
223
+ });
224
+ }
225
+ if (message.text) {
226
+ events.push({ type: "text_delta", messageId, delta: message.text });
227
+ }
228
+ for (const tool of message.tools) {
229
+ events.push({
230
+ type: "tool_call",
231
+ callId: tool.callId,
232
+ name: tool.name,
233
+ messageId,
234
+ view: tool.view,
235
+ });
236
+ if (tool.done) {
237
+ events.push({
238
+ type: "tool_end",
239
+ callId: tool.callId,
240
+ view: tool.view,
241
+ isError: tool.isError,
242
+ });
243
+ }
244
+ }
245
+ }
246
+ events.push(this.sessionState());
247
+ return events;
248
+ }
249
+
250
+ /** Broadcast an event the stream did not derive itself, e.g. a state push. */
251
+ public push(event: ServerEvent): void {
252
+ this.emit(event);
253
+ }
254
+
255
+ /** Drop the server's picker caches and tell every client to drop theirs. */
256
+ public invalidatePickers(scope: "files" | "commands" | "all"): void {
257
+ this.picker.invalidate();
258
+ this.emit({ type: "picker_invalidate", scope, cwd: this.host.cwd });
259
+ }
260
+
261
+ public sessionState(): EphemeralEvent {
262
+ const tps = this.host.tps;
263
+ const usage = this.host.usage();
264
+ const cwd = this.host.cwd;
265
+ const { branch, dirtyCount, ahead, behind, revision } =
266
+ this.git.stateOf(cwd);
267
+ const modelLabel = this.host.currentModelLabel;
268
+ const turnElapsedMs =
269
+ this.host.status === "idle" || this.turnStartedAt === 0
270
+ ? undefined
271
+ : Date.now() - this.turnStartedAt;
272
+ return {
273
+ type: "session_state",
274
+ cwd,
275
+ model: this.host.currentModelId ?? "",
276
+ ...(modelLabel === undefined ? {} : { modelLabel }),
277
+ thinking: this.host.currentThinkingLevel,
278
+ cost: this.host.settings.cumulativeCost ?? 0,
279
+ status: this.host.status,
280
+ ...this.leaseState(),
281
+ ...(this.repoBusy?.() === true ? { repoBusy: true } : {}),
282
+ ...(tps === undefined ? {} : { tps }),
283
+ ...(turnElapsedMs === undefined ? {} : { turnElapsedMs }),
284
+ ...(usage?.percent === null || usage === undefined
285
+ ? {}
286
+ : {
287
+ contextPercent: usage.percent,
288
+ contextWindow: usage.contextWindow,
289
+ }),
290
+ ...(branch === null
291
+ ? {}
292
+ : { branch, dirtyCount, ahead, behind, repoRevision: revision }),
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Two sources, and both are needed: the host knows only about a mutation of
298
+ * its own parked behind someone else, and reads writable for a session
299
+ * merely sitting idle under a foreign lease — which the cached record covers.
300
+ */
301
+ private leaseState(): LeaseState {
302
+ const own = this.host.leaseState;
303
+ if (!own.writable) {
304
+ return own;
305
+ }
306
+ const holder = this.holder;
307
+ // Our own turn: the lease exists because this process took it.
308
+ if (
309
+ holder === undefined ||
310
+ (holder.pid === process.pid && holder.frontend === "daemon")
311
+ ) {
312
+ return { writable: true };
313
+ }
314
+ return {
315
+ writable: false,
316
+ heldBy: { frontend: holder.frontend, pid: holder.pid },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Re-reads the holder; true when what a client would render changed. Our own
322
+ * lease comes and goes on every turn we take and says nothing, so only a
323
+ * holder that is not us ever moves this.
324
+ */
325
+ private async readLease(): Promise<boolean> {
326
+ this.holder = await SessionLease.read(this.sessionPath);
327
+ const next = this.leaseState();
328
+ if (sameLease(this.lease, next)) {
329
+ return false;
330
+ }
331
+ this.lease = next;
332
+ return true;
333
+ }
334
+
335
+ private async pushLease(): Promise<void> {
336
+ if (await this.readLease()) {
337
+ this.emit(this.sessionState());
338
+ }
339
+ }
340
+
341
+ public dispose(): void {
342
+ this.unsubscribe?.();
343
+ this.unsubscribe = undefined;
344
+ this.unsubscribeLease?.();
345
+ this.unsubscribeLease = undefined;
346
+ this.unsubscribeForeign?.();
347
+ this.unsubscribeForeign = undefined;
348
+ this.watchFiles(false);
349
+ this.listeners.clear();
350
+ }
351
+
352
+ private onAgentEvent(event: AgentSessionEvent): void {
353
+ switch (event.type) {
354
+ case "agent_start":
355
+ this.turnStartedAt = Date.now();
356
+ this.liveTurn = [];
357
+ this.emit(this.sessionState());
358
+ return;
359
+ case "message_start":
360
+ if (event.message.role === "assistant") {
361
+ this.startMessage();
362
+ this.emit(this.sessionState());
363
+ }
364
+ return;
365
+ case "message_update": {
366
+ if (event.message.role !== "assistant") {
367
+ return;
368
+ }
369
+ const message = this.currentMessage();
370
+ const text = MessageText.textOf(event.message.content);
371
+ const thinking = MessageText.textOf(event.message.content, "thinking");
372
+ this.emitDelta(message, "thinking", thinking);
373
+ this.emitDelta(message, "text", text);
374
+ return;
375
+ }
376
+ case "tool_execution_start": {
377
+ const view = Tools.viewOf({
378
+ name: event.toolName,
379
+ args: event.args,
380
+ isPartial: true,
381
+ cwd: this.host.cwd,
382
+ });
383
+ const message = this.currentMessage();
384
+ message.tools.push({
385
+ callId: event.toolCallId,
386
+ name: event.toolName,
387
+ args: event.args,
388
+ view,
389
+ isError: false,
390
+ done: false,
391
+ });
392
+ this.emit({
393
+ type: "tool_call",
394
+ callId: event.toolCallId,
395
+ name: event.toolName,
396
+ messageId: message.messageId,
397
+ view,
398
+ });
399
+ this.emit(this.sessionState());
400
+ return;
401
+ }
402
+ case "tool_execution_update": {
403
+ const view = Tools.viewOf({
404
+ name: event.toolName,
405
+ args: event.args,
406
+ result: event.partialResult,
407
+ isPartial: true,
408
+ cwd: this.host.cwd,
409
+ });
410
+ const tool = this.findTool(event.toolCallId);
411
+ if (tool) {
412
+ tool.view = view;
413
+ }
414
+ this.emit({ type: "tool_update", callId: event.toolCallId, view });
415
+ return;
416
+ }
417
+ case "tool_execution_end": {
418
+ const tool = this.findTool(event.toolCallId);
419
+ const view = Tools.viewOf({
420
+ name: event.toolName,
421
+ args: tool?.args,
422
+ result: event.result,
423
+ isError: event.isError,
424
+ isPartial: false,
425
+ cwd: this.host.cwd,
426
+ });
427
+ if (tool) {
428
+ tool.view = view;
429
+ tool.isError = event.isError;
430
+ tool.done = true;
431
+ }
432
+ this.emit({
433
+ type: "tool_end",
434
+ callId: event.toolCallId,
435
+ view,
436
+ isError: event.isError,
437
+ });
438
+ if (Tools.effectOf(event.toolName)?.kind !== "readOnly") {
439
+ this.invalidatePickers("files");
440
+ // The tool just wrote the worktree, which `.git` never reports.
441
+ void this.git.refresh(this.host.cwd);
442
+ }
443
+ return;
444
+ }
445
+ case "entry_appended":
446
+ void this.flushDurable();
447
+ return;
448
+ // Never await the flush: pi appends the entry only after this listener returns.
449
+ case "message_end": {
450
+ if (event.message.role === "assistant") {
451
+ const open = this.liveTurn.at(-1);
452
+ if (open) {
453
+ open.ended = true;
454
+ }
455
+ }
456
+ void this.flushDurable();
457
+ return;
458
+ }
459
+ case "turn_end": {
460
+ const usage =
461
+ event.message.role === "assistant" ? event.message.usage : undefined;
462
+ this.emit({
463
+ type: "turn_end",
464
+ stats: {
465
+ inputTokens: usage?.input ?? 0,
466
+ outputTokens: usage?.output ?? 0,
467
+ costUsd: usage?.cost.total ?? 0,
468
+ durationMs: Date.now() - this.turnStartedAt,
469
+ },
470
+ });
471
+ return;
472
+ }
473
+ case "agent_settled":
474
+ void this.flushDurable().then(() => {
475
+ // Clear only once the durable events superseding it are on the wire.
476
+ this.liveTurn = [];
477
+ this.emit(this.sessionState());
478
+ });
479
+ return;
480
+ default:
481
+ return;
482
+ }
483
+ }
484
+
485
+ private currentMessage(): LiveMessage {
486
+ return this.liveTurn.at(-1) ?? this.startMessage();
487
+ }
488
+
489
+ private startMessage(): LiveMessage {
490
+ this.liveMessageId += 1;
491
+ const message: LiveMessage = {
492
+ messageId: `${this.sessionId}:live:${this.liveMessageId}`,
493
+ text: "",
494
+ thinking: "",
495
+ ended: false,
496
+ retired: false,
497
+ tools: [],
498
+ };
499
+ this.liveTurn.push(message);
500
+ this.emit({
501
+ type: "message_start",
502
+ role: "assistant",
503
+ messageId: message.messageId,
504
+ });
505
+ return message;
506
+ }
507
+
508
+ // Pi re-states the whole message on each update; only a suffix is expressible as a delta.
509
+ private emitDelta(
510
+ message: LiveMessage,
511
+ channel: "text" | "thinking",
512
+ next: string
513
+ ): void {
514
+ const sent = message[channel];
515
+ if (!next.startsWith(sent) || next === sent) {
516
+ return;
517
+ }
518
+ this.emit({
519
+ type: channel === "text" ? "text_delta" : "thinking_delta",
520
+ messageId: message.messageId,
521
+ delta: next.slice(sent.length),
522
+ });
523
+ message[channel] = next;
524
+ }
525
+
526
+ private locate(
527
+ callId: string
528
+ ): { readonly message: LiveMessage; readonly at: number } | undefined {
529
+ for (const message of this.liveTurn) {
530
+ const at = message.tools.findIndex((tool) => tool.callId === callId);
531
+ if (at !== -1) {
532
+ return { message, at };
533
+ }
534
+ }
535
+ return undefined;
536
+ }
537
+
538
+ private findTool(callId: string): LiveTool | undefined {
539
+ const found = this.locate(callId);
540
+ return found === undefined ? undefined : found.message.tools[found.at];
541
+ }
542
+
543
+ /**
544
+ * Serialised: two reads in flight would race on `sentSeq` and hand a client
545
+ * the same lines twice, or out of order. The agent-event path and the file
546
+ * watch both come through here, so they can only ever queue behind one
547
+ * another.
548
+ */
549
+ private flushDurable(): Promise<void> {
550
+ const done = this.draining.then(() => this.drainDurable());
551
+ this.draining = done.catch(() => {});
552
+ return done;
553
+ }
554
+
555
+ // A burst of appends is one drain: a trigger waits for the read already running, and one arriving after that starts gets its own.
556
+ private scheduleDrain(): void {
557
+ if (this.drainQueued) {
558
+ return;
559
+ }
560
+ this.drainQueued = true;
561
+ void this.draining
562
+ .then(() => {
563
+ this.drainQueued = false;
564
+ return this.flushDurable();
565
+ })
566
+ .catch(() => {});
567
+ }
568
+
569
+ // One frame: a retire split from the durable message that caused it paints the step twice.
570
+ private async drainDurable(): Promise<void> {
571
+ await this.projection.drain();
572
+ // The watermark is what every listener has been offered, so one client's read cannot swallow another's tail.
573
+ const fresh = this.projection.since(this.sentSeq);
574
+ this.sentSeq = this.projection.head;
575
+ const batch: StreamEvent[] = [];
576
+ for (const event of fresh) {
577
+ batch.push(event);
578
+ if (event.type === "message" && event.role === "assistant") {
579
+ const retired = this.retireLive();
580
+ if (retired) {
581
+ batch.push(retired);
582
+ }
583
+ }
584
+ if (event.type === "tool_result") {
585
+ this.settleLive(event.callId);
586
+ }
587
+ }
588
+ if (batch.length > 0) {
589
+ this.emit({ type: "replay", events: batch });
590
+ }
591
+ }
592
+
593
+ // Retire the oldest *finished* message, never the oldest: a retired shell may still be collecting calls.
594
+ private retireLive(): EphemeralEvent | undefined {
595
+ const retired = this.liveTurn.find(
596
+ (message) => message.ended && !message.retired
597
+ );
598
+ if (!retired) {
599
+ return undefined;
600
+ }
601
+ retired.text = "";
602
+ retired.thinking = "";
603
+ retired.retired = true;
604
+ if (retired.tools.length === 0) {
605
+ this.liveTurn = this.liveTurn.filter((message) => message !== retired);
606
+ }
607
+ return { type: "message_retire", messageId: retired.messageId };
608
+ }
609
+
610
+ private settleLive(callId: string): void {
611
+ const found = this.locate(callId);
612
+ if (found === undefined) {
613
+ return;
614
+ }
615
+ const { message } = found;
616
+ message.tools.splice(found.at, 1);
617
+ if (message.retired && message.tools.length === 0) {
618
+ this.liveTurn = this.liveTurn.filter((held) => held !== message);
619
+ }
620
+ }
621
+
622
+ private emit(event: ServerEvent): void {
623
+ for (const listener of this.listeners) {
624
+ listener(event);
625
+ }
626
+ }
627
+ }