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,202 @@
1
+ import type {
2
+ ExtensionContext,
3
+ ThinkingLevelChangeEntry,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
6
+ import { Format } from "#core/shared/Format";
7
+ import { Paths } from "#core/shared/Paths";
8
+ import type { GitState } from "#core/shared/Git";
9
+ import {
10
+ BG_BRIGHT_GREEN,
11
+ BG_BRIGHT_MAGENTA,
12
+ BG_BRIGHT_RED,
13
+ BG_BRIGHT_YELLOW,
14
+ BG_GRAY,
15
+ FG_BLACK,
16
+ FG_WHITE,
17
+ GIT_AHEAD_ICON,
18
+ GIT_BEHIND_ICON,
19
+ GIT_DIRTY_ICON,
20
+ GIT_ICON,
21
+ groupWidth,
22
+ renderLeftGroup,
23
+ renderRightGroup,
24
+ type Segment,
25
+ thinChevronLeft,
26
+ } from "./powerline";
27
+
28
+ function gitSegment(state: GitState): Segment | null {
29
+ const { branch, dirtyCount, ahead, behind } = state;
30
+ if (!branch) {
31
+ return null;
32
+ }
33
+ let text = `${GIT_ICON} ${branch}`;
34
+ if (dirtyCount > 0) {
35
+ text += ` ${GIT_DIRTY_ICON}${dirtyCount}`;
36
+ }
37
+ if (ahead > 0 || behind > 0) {
38
+ let arrows = " ";
39
+ if (ahead > 0) {
40
+ arrows += `${GIT_AHEAD_ICON}${ahead}`;
41
+ }
42
+ if (behind > 0) {
43
+ arrows += `${GIT_BEHIND_ICON}${behind}`;
44
+ }
45
+ text += arrows;
46
+ }
47
+ const bg =
48
+ behind > 0
49
+ ? BG_BRIGHT_RED
50
+ : dirtyCount > 0
51
+ ? BG_BRIGHT_YELLOW
52
+ : BG_BRIGHT_GREEN;
53
+ return { text, fg: FG_BLACK, bg };
54
+ }
55
+
56
+ function ctxSegment(ctx: ExtensionContext): Segment | null {
57
+ const usage = ctx.getContextUsage();
58
+ if (!usage || usage.contextWindow === 0) {
59
+ return null;
60
+ }
61
+ const window = Format.formatTokens(usage.contextWindow);
62
+ const text =
63
+ usage.percent === null
64
+ ? `?/${window}`
65
+ : `${usage.percent.toFixed(1)}%/${window}`;
66
+ const bg = {
67
+ full: BG_BRIGHT_RED,
68
+ warn: BG_BRIGHT_YELLOW,
69
+ ok: BG_BRIGHT_GREEN,
70
+ }[Format.contextFill(usage.percent ?? 0)];
71
+ return { text, fg: FG_BLACK, bg };
72
+ }
73
+
74
+ function costSegment(cost: number): Segment | null {
75
+ if (cost <= 0) {
76
+ return null;
77
+ }
78
+ return {
79
+ text: `$${cost.toFixed(2)}`,
80
+ fg: FG_BLACK,
81
+ bg: BG_BRIGHT_MAGENTA,
82
+ };
83
+ }
84
+
85
+ const LEVEL_LABEL: Record<string, string> = { minimal: "min", medium: "med" };
86
+
87
+ function findLatestThinkingLevel(ctx: ExtensionContext): string {
88
+ const branch = ctx.sessionManager.getBranch();
89
+ for (let i = branch.length - 1; i >= 0; i--) {
90
+ const entry = branch[i]!;
91
+ if (entry.type === "thinking_level_change") {
92
+ return (entry as ThinkingLevelChangeEntry).thinkingLevel;
93
+ }
94
+ }
95
+ return "off";
96
+ }
97
+
98
+ function modelSegment(ctx: ExtensionContext): Segment | null {
99
+ const id = ctx.model?.id;
100
+ if (!id) {
101
+ return null;
102
+ }
103
+
104
+ if (ctx.model?.reasoning) {
105
+ const level = findLatestThinkingLevel(ctx);
106
+ const label = LEVEL_LABEL[level] ?? level;
107
+ return {
108
+ text: `${id} ${thinChevronLeft(BG_GRAY, FG_WHITE)} ${label}`,
109
+ fg: FG_WHITE,
110
+ bg: BG_GRAY,
111
+ };
112
+ }
113
+
114
+ return { text: id, fg: FG_WHITE, bg: BG_GRAY };
115
+ }
116
+
117
+ function compact<T>(items: readonly (T | null)[]): T[] {
118
+ return items.filter((x): x is T => x !== null);
119
+ }
120
+
121
+ function fitLine(line: string, width: number): string {
122
+ if (width <= 0) {
123
+ return "";
124
+ }
125
+ return visibleWidth(line) <= width ? line : truncateToWidth(line, width, "");
126
+ }
127
+
128
+ const bold = (s: string): string => `\x1b[1m${s}\x1b[22m`;
129
+
130
+ function formatCwd(path: string): string {
131
+ if (path === "/" || path === "~") {
132
+ return bold(path);
133
+ }
134
+ const parts = path.split("/");
135
+ const last = parts.pop()!;
136
+ const abbrParent = parts
137
+ .map((p) => {
138
+ if (p === "" || p === "~") {
139
+ return p;
140
+ }
141
+ return p.startsWith(".") && p.length > 1 ? p.slice(0, 2) : p[0]!;
142
+ })
143
+ .join("/");
144
+ return `${abbrParent}/${bold(last)}`;
145
+ }
146
+
147
+ export function renderFooterLine(
148
+ width: number,
149
+ ctx: ExtensionContext,
150
+ gitState: GitState,
151
+ cost: number
152
+ ): string {
153
+ const cwd: Segment = {
154
+ text: formatCwd(Paths.abbreviateHome(ctx.sessionManager.getCwd())),
155
+ fg: FG_WHITE,
156
+ bg: BG_GRAY,
157
+ };
158
+ const branch = gitSegment(gitState);
159
+ const costSeg = costSegment(cost);
160
+ const ctxSeg = ctxSegment(ctx);
161
+ const model = modelSegment(ctx);
162
+
163
+ const fullLeft = compact([cwd, branch]);
164
+ const candidates: readonly { left: Segment[]; right: Segment[] }[] = [
165
+ { left: fullLeft, right: compact([costSeg, ctxSeg, model]) },
166
+ { left: fullLeft, right: compact([costSeg, ctxSeg]) },
167
+ { left: fullLeft, right: compact([ctxSeg]) },
168
+ { left: [cwd], right: compact([ctxSeg]) },
169
+ { left: [cwd], right: [] },
170
+ ];
171
+
172
+ const gapOf = (c: { left: Segment[]; right: Segment[] }): number =>
173
+ c.left.length > 0 && c.right.length > 0 ? 1 : 0;
174
+ const fits = (c: { left: Segment[]; right: Segment[] }): boolean =>
175
+ groupWidth(c.left) + groupWidth(c.right) + gapOf(c) <= width;
176
+
177
+ const chosen = candidates.find(fits) ?? candidates.at(-1)!;
178
+ const rightWidth = groupWidth(chosen.right);
179
+
180
+ let left = chosen.left;
181
+ let leftWidth = groupWidth(left);
182
+ const requiredWidth = leftWidth + rightWidth + gapOf(chosen);
183
+ if (requiredWidth > width && left.length > 0) {
184
+ const overflow = requiredWidth - width;
185
+ const newCwdWidth = Math.max(0, visibleWidth(left[0]!.text) - overflow);
186
+ const truncated: Segment = {
187
+ ...left[0]!,
188
+ text: truncateToWidth(left[0]!.text, newCwdWidth, "…"),
189
+ };
190
+ left = [truncated, ...left.slice(1)];
191
+ leftWidth = groupWidth(left);
192
+ }
193
+
194
+ const gap =
195
+ left.length > 0 && chosen.right.length > 0
196
+ ? Math.max(1, width - leftWidth - rightWidth)
197
+ : 0;
198
+ return fitLine(
199
+ renderLeftGroup(left) + " ".repeat(gap) + renderRightGroup(chosen.right),
200
+ width
201
+ );
202
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ DynamicBorder,
3
+ type ExtensionAPI,
4
+ getSettingsListTheme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ type Component,
8
+ Container,
9
+ type SettingItem,
10
+ SettingsList,
11
+ } from "@earendil-works/pi-tui";
12
+ import {
13
+ ExtensionToggles,
14
+ type PimExtensionName,
15
+ } from "#core/shared/ExtensionToggles";
16
+
17
+ const ENABLED = "enabled";
18
+ const DISABLED = "disabled";
19
+
20
+ const MAX_VISIBLE = 12;
21
+
22
+ export async function menuItems(): Promise<SettingItem[]> {
23
+ const disabled = new Set(await ExtensionToggles.disabled());
24
+ return ExtensionToggles.NAMES.filter(
25
+ (name) => !ExtensionToggles.isRequired(name)
26
+ ).map((name) => ({
27
+ id: name,
28
+ label: name,
29
+ description: ExtensionToggles.describe(name),
30
+ currentValue: disabled.has(name) ? DISABLED : ENABLED,
31
+ values: [ENABLED, DISABLED],
32
+ }));
33
+ }
34
+
35
+ export function createToggleMenu(
36
+ items: SettingItem[],
37
+ onToggle: (name: PimExtensionName, disabled: boolean) => void,
38
+ done: () => void
39
+ ): Component {
40
+ const container = new Container();
41
+ container.addChild(new DynamicBorder());
42
+ const list = new SettingsList(
43
+ items,
44
+ Math.min(items.length, MAX_VISIBLE),
45
+ getSettingsListTheme(),
46
+ (id, value) => {
47
+ onToggle(id as PimExtensionName, value === DISABLED);
48
+ },
49
+ done
50
+ );
51
+ container.addChild(list);
52
+ container.addChild(new DynamicBorder());
53
+ return {
54
+ invalidate: () => container.invalidate(),
55
+ render: (width) => container.render(width),
56
+ handleInput: (data) => list.handleInput(data),
57
+ };
58
+ }
59
+
60
+ export default function (pi: ExtensionAPI): void {
61
+ pi.registerCommand("pim", {
62
+ description: "Enable or disable pim extensions",
63
+ handler: async (_args, ctx) => {
64
+ if (ctx.mode !== "tui") {
65
+ ctx.ui.notify("/pim needs the interactive TUI", "warning");
66
+ return;
67
+ }
68
+
69
+ const writes: Promise<void>[] = [];
70
+ const items = await menuItems();
71
+ await ctx.ui.custom<void>((_tui, _theme, _keybindings, done) =>
72
+ createToggleMenu(
73
+ items,
74
+ (name, disabled) => {
75
+ writes.push(ExtensionToggles.setDisabled(name, disabled));
76
+ },
77
+ () => done()
78
+ )
79
+ );
80
+ if (writes.length === 0) {
81
+ return;
82
+ }
83
+ await Promise.all(writes);
84
+
85
+ // `ctx` is stale after this: `reload()` re-invokes every inline factory.
86
+ await ctx.reload();
87
+ },
88
+ });
89
+ }
@@ -0,0 +1,248 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import { EventLog } from "#core/session/EventLog";
7
+ import {
8
+ type LeaseHandle,
9
+ type LeaseRecord,
10
+ SessionLease,
11
+ } from "#core/session/SessionLease";
12
+ import { WriteMark } from "#core/session/WriteMark";
13
+
14
+ const POLL_MS = 2_000;
15
+
16
+ /** Survives the extension instance: `switchSession` re-runs every factory. */
17
+ let carried: { readonly path: string; readonly text: string } | undefined;
18
+
19
+ function isForeign(record: LeaseRecord | undefined): record is LeaseRecord {
20
+ return record !== undefined && !SessionLease.isOurs(record);
21
+ }
22
+
23
+ function describe(record: LeaseRecord): string {
24
+ return record.frontend === "daemon" ? "The browser" : "Another terminal";
25
+ }
26
+
27
+ function notify(
28
+ ctx: ExtensionContext,
29
+ message: string,
30
+ level: "info" | "warning" | "error" = "warning"
31
+ ): void {
32
+ if (ctx.hasUI) {
33
+ ctx.ui.notify(message, level);
34
+ }
35
+ }
36
+
37
+ export default function (pi: ExtensionAPI): void {
38
+ let manager: ExtensionContext["sessionManager"] | undefined;
39
+ let path: string | undefined;
40
+ let log: EventLog | undefined;
41
+ let mark: WriteMark = WriteMark.UNREAD;
42
+ let stale = false;
43
+ let holder: LeaseRecord | undefined;
44
+ let handle: LeaseHandle | undefined;
45
+ let syncing = false;
46
+ let stopWatch: (() => void) | undefined;
47
+ let poll: ReturnType<typeof setInterval> | undefined;
48
+
49
+ const measure = async (): Promise<WriteMark | undefined> => {
50
+ if (log === undefined || manager === undefined) {
51
+ return undefined;
52
+ }
53
+ return await WriteMark.of(log, manager);
54
+ };
55
+
56
+ /** Absorb our own appends; anything else is another surface writing this session. */
57
+ const reconcile = async (): Promise<void> => {
58
+ const next = await measure();
59
+ if (next === undefined) {
60
+ return;
61
+ }
62
+ if (WriteMark.foreignSince(mark, next)) {
63
+ stale = true;
64
+ return;
65
+ }
66
+ mark = next;
67
+ };
68
+
69
+ /** Whatever the file says right now is the truth pi was opened on. */
70
+ const anchor = async (): Promise<void> => {
71
+ mark = (await measure()) ?? WriteMark.UNREAD;
72
+ stale = false;
73
+ };
74
+
75
+ const refresh = async (): Promise<void> => {
76
+ const current = path;
77
+ if (current === undefined) {
78
+ return;
79
+ }
80
+ holder = await SessionLease.read(current);
81
+ await reconcile();
82
+ };
83
+
84
+ const stopIdleWatch = (): void => {
85
+ stopWatch?.();
86
+ stopWatch = undefined;
87
+ if (poll !== undefined) {
88
+ clearInterval(poll);
89
+ poll = undefined;
90
+ }
91
+ };
92
+
93
+ // Idle only: during our own turn every append is ours and the lease is held.
94
+ const startIdleWatch = (): void => {
95
+ stopIdleWatch();
96
+ const current = path;
97
+ if (current === undefined) {
98
+ return;
99
+ }
100
+ const tick = (): void => {
101
+ void refresh().catch(() => undefined);
102
+ };
103
+ stopWatch = SessionLease.watch(current, tick);
104
+ poll = setInterval(tick, POLL_MS);
105
+ poll.unref?.();
106
+ };
107
+
108
+ const pointAt = (next: string | undefined): void => {
109
+ path = next;
110
+ log = next === undefined ? undefined : new EventLog(next);
111
+ mark = WriteMark.UNREAD;
112
+ stale = false;
113
+ holder = undefined;
114
+ };
115
+
116
+ const release = async (): Promise<void> => {
117
+ const held = handle;
118
+ handle = undefined;
119
+ await held?.release();
120
+ };
121
+
122
+ pi.on("session_start", async (_event, ctx) => {
123
+ manager = ctx.sessionManager;
124
+ handle = undefined;
125
+ syncing = false;
126
+ pointAt(ctx.sessionManager.getSessionFile());
127
+ await anchor();
128
+ startIdleWatch();
129
+
130
+ const stash = carried;
131
+ carried = undefined;
132
+ if (stash !== undefined && stash.path === path) {
133
+ notify(ctx, `Caught up — your message was not sent: ${stash.text}`);
134
+ }
135
+ });
136
+
137
+ pi.on("input", async (event, ctx) => {
138
+ if (event.streamingBehavior !== undefined || path === undefined) {
139
+ return { action: "continue" };
140
+ }
141
+ await refresh();
142
+
143
+ if (isForeign(holder)) {
144
+ notify(
145
+ ctx,
146
+ `${describe(holder)} is running a turn. Try again when it finishes.`
147
+ );
148
+ return { action: "handled" };
149
+ }
150
+ if (!stale) {
151
+ return { action: "continue" };
152
+ }
153
+
154
+ if (event.text.trim() !== "") {
155
+ carried = { path, text: event.text };
156
+ }
157
+ notify(ctx, "This session was continued elsewhere — catching up.");
158
+ if (!syncing) {
159
+ syncing = true;
160
+ // Deferred: dispatching inside `emitInput` tears the session down under its own stack.
161
+ setTimeout(() => {
162
+ pi.sendUserMessage("/sync", { expandPromptTemplates: true });
163
+ }, 0);
164
+ }
165
+ return { action: "handled" };
166
+ });
167
+
168
+ pi.on("before_agent_start", async (_event, ctx) => {
169
+ stopIdleWatch();
170
+ const current = path;
171
+ if (current === undefined || handle !== undefined) {
172
+ return;
173
+ }
174
+ const result = await SessionLease.acquire(current, "tui");
175
+ if (result.ok) {
176
+ handle = result.handle;
177
+ return;
178
+ }
179
+ notify(
180
+ ctx,
181
+ isForeign(result.holder)
182
+ ? `${describe(result.holder)} just started a turn. Try again when it finishes.`
183
+ : "This session's turn lease is held elsewhere. Try again in a moment."
184
+ );
185
+ ctx.abort();
186
+ });
187
+
188
+ pi.on("agent_settled", async (_event, ctx) => {
189
+ await release();
190
+ manager = ctx.sessionManager;
191
+ const file = ctx.sessionManager.getSessionFile();
192
+ if (file !== path) {
193
+ pointAt(file);
194
+ }
195
+ await reconcile();
196
+ startIdleWatch();
197
+ });
198
+
199
+ pi.on("session_shutdown", async () => {
200
+ stopIdleWatch();
201
+ await release();
202
+ });
203
+
204
+ pi.registerCommand("sync", {
205
+ description: "Reload this session from disk after another surface wrote it",
206
+ handler: async (_args, ctx) => {
207
+ syncing = false;
208
+ const current = path;
209
+ if (current === undefined) {
210
+ notify(ctx, "This session is not on disk yet — nothing to sync.");
211
+ return;
212
+ }
213
+ if (!ctx.isIdle()) {
214
+ notify(ctx, "Wait for the current turn to finish, then run /sync.");
215
+ return;
216
+ }
217
+ // A missing or unreadable file makes pi's session replacement exit the process.
218
+ if (!(await Bun.file(current).exists())) {
219
+ notify(ctx, `Session file is gone: ${current}`, "error");
220
+ return;
221
+ }
222
+ const held = await SessionLease.read(current);
223
+ if (isForeign(held)) {
224
+ notify(
225
+ ctx,
226
+ `${describe(held)} is running a turn. Try /sync when it finishes.`
227
+ );
228
+ return;
229
+ }
230
+ try {
231
+ // Reopening repairs a torn tail and rewrites on migration, so hold the lease over it.
232
+ await SessionLease.hold(
233
+ current,
234
+ "tui",
235
+ async () => {
236
+ const result = await ctx.switchSession(current);
237
+ if (result.cancelled) {
238
+ notify(ctx, "Sync cancelled.");
239
+ }
240
+ },
241
+ { timeoutMs: 0 }
242
+ );
243
+ } catch (err) {
244
+ notify(ctx, err instanceof Error ? err.message : String(err), "error");
245
+ }
246
+ },
247
+ });
248
+ }
@@ -0,0 +1,118 @@
1
+ import type { AssistantMessageEvent } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+
4
+ type RequestTiming = {
5
+ readonly sentMs: number;
6
+ firstOutputMs: number | null;
7
+ };
8
+
9
+ function isOutputEvent(event: AssistantMessageEvent): boolean {
10
+ switch (event.type) {
11
+ case "text_delta":
12
+ case "thinking_delta":
13
+ case "toolcall_delta":
14
+ return event.delta.length > 0;
15
+ case "text_end":
16
+ case "thinking_end":
17
+ return event.content.length > 0;
18
+ case "toolcall_end":
19
+ return true;
20
+ default:
21
+ return false;
22
+ }
23
+ }
24
+
25
+ export default function (pi: ExtensionAPI): void {
26
+ let requestTiming: RequestTiming | null = null;
27
+
28
+ let promptTokens = 0;
29
+ let prefillMs = 0;
30
+ let outputTokens = 0;
31
+ let decodeMs = 0;
32
+ let cacheReadTokens = 0;
33
+ let firstTtftMs: number | null = null;
34
+
35
+ pi.on("agent_start", () => {
36
+ promptTokens = 0;
37
+ prefillMs = 0;
38
+ outputTokens = 0;
39
+ decodeMs = 0;
40
+ cacheReadTokens = 0;
41
+ firstTtftMs = null;
42
+ });
43
+
44
+ pi.on("before_provider_request", () => {
45
+ requestTiming = {
46
+ sentMs: Date.now(),
47
+ firstOutputMs: null,
48
+ };
49
+ });
50
+
51
+ pi.on("message_update", (event) => {
52
+ if (
53
+ event.message.role === "assistant" &&
54
+ requestTiming !== null &&
55
+ requestTiming.firstOutputMs === null &&
56
+ isOutputEvent(event.assistantMessageEvent)
57
+ ) {
58
+ requestTiming.firstOutputMs = Date.now();
59
+ }
60
+ });
61
+
62
+ pi.on("message_end", (event) => {
63
+ if (event.message.role !== "assistant") {
64
+ return;
65
+ }
66
+
67
+ const timing = requestTiming;
68
+ const endedMs = Date.now();
69
+ requestTiming = null;
70
+ if (timing === null) {
71
+ return;
72
+ }
73
+
74
+ const usage = event.message.usage;
75
+ const responseMs = timing.firstOutputMs ?? endedMs;
76
+ const ttft = responseMs - timing.sentMs;
77
+ const decode = endedMs - responseMs;
78
+
79
+ if (firstTtftMs === null && ttft > 0) {
80
+ firstTtftMs = ttft;
81
+ }
82
+
83
+ const prefillCounted = (usage.input ?? 0) + (usage.cacheWrite ?? 0);
84
+ if (prefillCounted > 0 && ttft > 0) {
85
+ promptTokens += prefillCounted;
86
+ prefillMs += ttft;
87
+ }
88
+ if ((usage.output ?? 0) > 0 && decode > 0) {
89
+ outputTokens += usage.output;
90
+ decodeMs += decode;
91
+ }
92
+ cacheReadTokens += usage.cacheRead ?? 0;
93
+ });
94
+
95
+ pi.on("agent_end", (_event, ctx) => {
96
+ if (!ctx.hasUI) {
97
+ return;
98
+ }
99
+ if (decodeMs <= 0 && prefillMs <= 0) {
100
+ return;
101
+ }
102
+
103
+ const decodeTps = decodeMs > 0 ? outputTokens / (decodeMs / 1000) : 0;
104
+ const prefillTps = prefillMs > 0 ? promptTokens / (prefillMs / 1000) : 0;
105
+ const ttftSec = firstTtftMs !== null ? firstTtftMs / 1000 : 0;
106
+
107
+ const parts = [
108
+ `Decode: ${decodeTps.toFixed(1)} tps`,
109
+ `Prefill: ${prefillTps.toFixed(1)} tps`,
110
+ ];
111
+ if (cacheReadTokens > 0) {
112
+ parts.push(`Cache read: ${cacheReadTokens.toLocaleString()}`);
113
+ }
114
+ parts.push(`TTFT: ${ttftSec.toFixed(2)}s`);
115
+
116
+ ctx.ui.notify(parts.join(" | "), "info");
117
+ });
118
+ }