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,456 @@
1
+ import type {
2
+ AgentSessionEvent,
3
+ AgentToolResult,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { GrammyError, type Api } from "grammy";
6
+
7
+ import { Tools } from "#core/shared/Tools";
8
+ import { MarkdownPainter } from "#core/view/MarkdownPainter";
9
+ import type { LogsMode } from "./Config";
10
+ import { Markdown } from "./Markdown";
11
+ import type { Session, SessionId } from "./Session";
12
+ import { BR, TelegramHtml } from "./TelegramHtml";
13
+ import { TypingIndicator } from "./TypingIndicator";
14
+
15
+ export type TurnEndState = "ok" | "cancelled" | "error";
16
+ type TurnState = TurnEndState | "running";
17
+
18
+ type ToolEntry = {
19
+ readonly kind: "tool";
20
+ icon: string;
21
+ label: string;
22
+ state: "running" | "ok" | "error";
23
+ };
24
+
25
+ type ProseKind = "thinking" | "narration";
26
+
27
+ type ProseEntry = {
28
+ readonly kind: ProseKind;
29
+ readonly label: string;
30
+ html?: string;
31
+ };
32
+
33
+ type TrackerEntry = ToolEntry | ProseEntry;
34
+
35
+ const PROSE_OPTS = {
36
+ thinking: { italics: true },
37
+ narration: {},
38
+ } as const;
39
+
40
+ type ToolCall = {
41
+ readonly index: number;
42
+ readonly toolName: string;
43
+ readonly args: unknown;
44
+ };
45
+
46
+ export class Renderer {
47
+ private readonly api: Api;
48
+ private readonly sessionId: SessionId;
49
+ private readonly logsMode: LogsMode;
50
+ private readonly entries: TrackerEntry[] = [];
51
+ private readonly calls = new Map<string, ToolCall>();
52
+ private readonly cwd: string;
53
+ private readonly typing: TypingIndicator;
54
+ private statusMessageId: number | undefined;
55
+ private editTimer: Timer | undefined;
56
+ private thinking = "";
57
+ private narration = "";
58
+ private currentMessageText = "";
59
+ private streamedFinalText = "";
60
+ private pendingNarrationCount = 0;
61
+ private lastRendered = "";
62
+ private stopped = false;
63
+
64
+ public constructor(session: Session, api: Api) {
65
+ this.api = api;
66
+ this.sessionId = session.id;
67
+ this.cwd = session.cwd;
68
+ this.logsMode = session.settings.logsMode ?? "text";
69
+ this.typing = new TypingIndicator(api, session.id);
70
+ }
71
+
72
+ public start(): void {
73
+ this.typing.start();
74
+ }
75
+
76
+ public handleEvent(event: AgentSessionEvent): void {
77
+ if (this.stopped) {
78
+ return;
79
+ }
80
+ if (event.type === "message_update") {
81
+ const update = event.assistantMessageEvent as {
82
+ readonly type: string;
83
+ readonly delta?: string;
84
+ readonly content?: string;
85
+ };
86
+ if (update.type === "thinking_delta") {
87
+ this.thinking += update.delta ?? "";
88
+ return;
89
+ }
90
+ if (update.type === "thinking_end") {
91
+ this.thinking = update.content ?? this.thinking;
92
+ this.flushThinking();
93
+ return;
94
+ }
95
+ if (update.type === "text_delta") {
96
+ this.flushThinking();
97
+ this.narration += update.delta ?? "";
98
+ this.currentMessageText += update.delta ?? "";
99
+ return;
100
+ }
101
+ if (update.type === "text_end") {
102
+ this.narration = update.content ?? this.narration;
103
+ this.pushNarration();
104
+ return;
105
+ }
106
+ this.flushThinking();
107
+ return;
108
+ }
109
+ if (event.type === "message_start") {
110
+ this.flushThinking();
111
+ this.narration = "";
112
+ this.currentMessageText = "";
113
+ this.pendingNarrationCount = 0;
114
+ return;
115
+ }
116
+ if (event.type === "tool_execution_start") {
117
+ this.flushThinking();
118
+ if (this.logsMode === "off") {
119
+ return;
120
+ }
121
+ this.addTool(event.toolCallId, event.toolName, event.args);
122
+ return;
123
+ }
124
+ if (event.type === "tool_execution_update") {
125
+ if (this.logsMode === "off") {
126
+ return;
127
+ }
128
+ this.refreshTool(event.toolCallId, event.partialResult, true);
129
+ return;
130
+ }
131
+ if (event.type === "tool_execution_end") {
132
+ if (this.logsMode === "off") {
133
+ return;
134
+ }
135
+ // A failed call's result is pi's synthetic empty error; repainting drops the row's details.
136
+ if (!event.isError) {
137
+ this.refreshTool(event.toolCallId, event.result, false);
138
+ }
139
+ const call = this.calls.get(event.toolCallId);
140
+ const entry = call === undefined ? undefined : this.entries[call.index];
141
+ if (entry?.kind === "tool") {
142
+ entry.state = event.isError ? "error" : "ok";
143
+ this.scheduleEdit();
144
+ }
145
+ return;
146
+ }
147
+ if (event.type === "message_end") {
148
+ this.flushThinking();
149
+ this.settleMessageNarrations(event.message);
150
+ return;
151
+ }
152
+ if (event.type === "agent_end") {
153
+ this.flushThinking();
154
+ this.narration = "";
155
+ }
156
+ }
157
+
158
+ public async finish(finalText: string, state: TurnEndState): Promise<void> {
159
+ this.stopped = true;
160
+ this.clearTimers();
161
+ this.flushThinking();
162
+ this.narration = "";
163
+ await this.flushEdit(state);
164
+ const textToSend = finalText.trim()
165
+ ? finalText
166
+ : this.streamedFinalText.trim();
167
+ if (textToSend) {
168
+ await this.sendFinal(textToSend);
169
+ }
170
+ }
171
+
172
+ private addTool(toolCallId: string, toolName: string, args: unknown): void {
173
+ const { icon, label } = this.paintTool(toolName, args, undefined, true);
174
+ const last = this.entries.at(-1);
175
+
176
+ if (last?.kind === "tool" && last.icon === icon && last.label === label) {
177
+ last.state = "running";
178
+ } else {
179
+ this.entries.push({ kind: "tool", icon, label, state: "running" });
180
+ }
181
+
182
+ this.calls.set(toolCallId, {
183
+ index: this.entries.length - 1,
184
+ toolName,
185
+ args,
186
+ });
187
+ this.scheduleEdit();
188
+ }
189
+
190
+ private refreshTool(
191
+ toolCallId: string,
192
+ result: unknown,
193
+ isPartial: boolean
194
+ ): void {
195
+ const call = this.calls.get(toolCallId);
196
+ if (call === undefined) {
197
+ return;
198
+ }
199
+ const { icon, label } = this.paintTool(
200
+ call.toolName,
201
+ call.args,
202
+ result as AgentToolResult<unknown> | undefined,
203
+ isPartial
204
+ );
205
+ const entry = this.entries[call.index];
206
+ if (entry?.kind !== "tool") {
207
+ return;
208
+ }
209
+ if (entry.icon === icon && entry.label === label) {
210
+ return;
211
+ }
212
+ entry.icon = icon;
213
+ entry.label = label;
214
+ this.scheduleEdit();
215
+ }
216
+
217
+ private paintTool(
218
+ toolName: string,
219
+ args: unknown,
220
+ result: AgentToolResult<unknown> | undefined,
221
+ isPartial: boolean
222
+ ): { readonly icon: string; readonly label: string } {
223
+ const painted = MarkdownPainter.paintTool(
224
+ Tools.viewOf({
225
+ name: toolName,
226
+ args,
227
+ ...(result === undefined ? {} : { result }),
228
+ isPartial,
229
+ cwd: this.cwd,
230
+ })
231
+ );
232
+ return { icon: painted.icon, label: painted.lines.join(BR) };
233
+ }
234
+
235
+ private flushThinking(): void {
236
+ const raw = this.thinking;
237
+ this.thinking = "";
238
+ if (this.logsMode !== "verbose") {
239
+ return;
240
+ }
241
+ this.pushProse("thinking", raw);
242
+ }
243
+
244
+ private pushNarration(): void {
245
+ const raw = this.narration;
246
+ this.narration = "";
247
+ if (this.logsMode !== "text" && this.logsMode !== "verbose") {
248
+ return;
249
+ }
250
+ if (this.pushProse("narration", raw)) {
251
+ this.pendingNarrationCount += 1;
252
+ }
253
+ }
254
+
255
+ private pushProse(kind: ProseKind, raw: string): boolean {
256
+ const text = cleanProse(raw);
257
+ if (!text) {
258
+ return false;
259
+ }
260
+ const last = this.entries.at(-1);
261
+ if (last?.kind === kind && last.label === text) {
262
+ return false;
263
+ }
264
+ this.entries.push({ kind, label: text });
265
+ this.scheduleEdit();
266
+ return true;
267
+ }
268
+
269
+ private settleMessageNarrations(message: unknown): void {
270
+ const msg = message as {
271
+ readonly role?: string;
272
+ readonly stopReason?: string;
273
+ };
274
+ const isFinal = msg.role === "assistant" && msg.stopReason !== "toolUse";
275
+ if (isFinal) {
276
+ this.streamedFinalText = this.currentMessageText;
277
+ const before = this.entries.length;
278
+ for (let i = 0; i < this.pendingNarrationCount; i++) {
279
+ if (this.entries.at(-1)?.kind !== "narration") {
280
+ break;
281
+ }
282
+ this.entries.pop();
283
+ }
284
+ if (this.entries.length !== before) {
285
+ this.scheduleEdit();
286
+ }
287
+ }
288
+ this.pendingNarrationCount = 0;
289
+ }
290
+
291
+ private scheduleEdit(): void {
292
+ if (this.logsMode === "off") {
293
+ return;
294
+ }
295
+ if (this.editTimer) {
296
+ return;
297
+ }
298
+ this.editTimer = setTimeout(() => {
299
+ this.editTimer = undefined;
300
+ if (this.stopped) {
301
+ return;
302
+ }
303
+ void this.flushEdit("running");
304
+ }, 1_000);
305
+ }
306
+
307
+ private async flushEdit(state: TurnState): Promise<void> {
308
+ if (this.editTimer) {
309
+ clearTimeout(this.editTimer);
310
+ this.editTimer = undefined;
311
+ }
312
+ if (this.logsMode === "off") {
313
+ return;
314
+ }
315
+ const body = this.renderStatus(state);
316
+ if (!body) {
317
+ return;
318
+ }
319
+ if (body === this.lastRendered) {
320
+ return;
321
+ }
322
+ this.lastRendered = body;
323
+ if (this.statusMessageId === undefined) {
324
+ const msg = await this.sendMessage(body, { status: true });
325
+ this.statusMessageId = msg?.message_id;
326
+ return;
327
+ }
328
+ await this.editMessage(body);
329
+ }
330
+
331
+ private renderStatus(state: TurnState): string {
332
+ const visible = this.entries;
333
+ const pieces: string[] = [];
334
+ if (visible.length === 0) {
335
+ return "";
336
+ }
337
+ for (let i = 0; i < visible.length; i++) {
338
+ const entry = visible[i]!;
339
+ if (entry.kind === "tool") {
340
+ const isLastEntry = i === visible.length - 1;
341
+ let suffix = "";
342
+ if (entry.state === "error") {
343
+ suffix = " ❌";
344
+ } else if (state === "running" && isLastEntry) {
345
+ suffix = " 🟡";
346
+ }
347
+ pieces.push(`${entry.icon} ${entry.label}${suffix}`);
348
+ } else {
349
+ entry.html ??= Markdown.toHtml(entry.label, PROSE_OPTS[entry.kind]);
350
+ pieces.push(entry.html);
351
+ }
352
+ const next = visible[i + 1];
353
+ if (next && isInlineEntry(entry) && isInlineEntry(next)) {
354
+ pieces.push(BR);
355
+ }
356
+ }
357
+
358
+ let body = pieces.join("");
359
+ if (state === "cancelled") {
360
+ body += "<br><br>❌ Cancelled";
361
+ } else if (state === "error") {
362
+ body += "<br><br>❌ Error";
363
+ }
364
+ return TelegramHtml.cap(body);
365
+ }
366
+
367
+ private async sendFinal(markdown: string): Promise<void> {
368
+ const html = Markdown.toHtml(markdown);
369
+ for (const piece of TelegramHtml.chunk(html)) {
370
+ await this.sendMessage(piece, { status: false });
371
+ }
372
+ }
373
+
374
+ private async sendMessage(
375
+ html: string,
376
+ opts: { readonly status: boolean }
377
+ ): Promise<{ readonly message_id: number } | undefined> {
378
+ if (!html) {
379
+ return undefined;
380
+ }
381
+ const clean = TelegramHtml.sanitize(html);
382
+ try {
383
+ const msg = await this.api.sendRichMessage(
384
+ this.sessionId.chatId,
385
+ { html: clean },
386
+ { message_thread_id: this.sessionId.threadId }
387
+ );
388
+ console.log(
389
+ `[send] chatId=${this.sessionId.chatId} threadId=${this.sessionId.threadId ?? "main"} ${opts.status ? "status" : "answer"} ok (${clean.length}b)`
390
+ );
391
+ return msg;
392
+ } catch (err) {
393
+ if (err instanceof GrammyError && err.error_code === 400) {
394
+ console.warn(`[send] rich 400 (${err.description}) — retry plain`);
395
+ return this.api.sendMessage(
396
+ this.sessionId.chatId,
397
+ TelegramHtml.strip(clean),
398
+ {
399
+ message_thread_id: this.sessionId.threadId,
400
+ link_preview_options: { is_disabled: true },
401
+ }
402
+ );
403
+ }
404
+ throw err;
405
+ }
406
+ }
407
+
408
+ private async editMessage(html: string): Promise<void> {
409
+ const clean = TelegramHtml.sanitize(html);
410
+ try {
411
+ await this.api.editMessageText(
412
+ this.sessionId.chatId,
413
+ this.statusMessageId!,
414
+ {
415
+ html: clean,
416
+ }
417
+ );
418
+ } catch (err) {
419
+ if (err instanceof GrammyError) {
420
+ if (/message is not modified/i.test(err.description)) {
421
+ return;
422
+ }
423
+ if (err.error_code === 400) {
424
+ await this.api
425
+ .editMessageText(
426
+ this.sessionId.chatId,
427
+ this.statusMessageId!,
428
+ TelegramHtml.strip(clean),
429
+ {
430
+ link_preview_options: { is_disabled: true },
431
+ }
432
+ )
433
+ .catch(() => {});
434
+ return;
435
+ }
436
+ }
437
+ console.warn(`[send] status edit failed:`, err);
438
+ }
439
+ }
440
+
441
+ private clearTimers(): void {
442
+ this.typing.stop();
443
+ if (this.editTimer) {
444
+ clearTimeout(this.editTimer);
445
+ this.editTimer = undefined;
446
+ }
447
+ }
448
+ }
449
+
450
+ function isInlineEntry(entry: TrackerEntry): boolean {
451
+ return entry.kind === "tool";
452
+ }
453
+
454
+ function cleanProse(text: string): string {
455
+ return text.replace(/\n{3,}/g, "\n\n").trim();
456
+ }
@@ -0,0 +1,18 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const MAX_CAPTION_CHARS = 1024;
4
+
5
+ export const sendFileSchema = Type.Object({
6
+ path: Type.String({
7
+ minLength: 1,
8
+ description: "Absolute or relative path to file (resolved against cwd).",
9
+ }),
10
+ caption: Type.Optional(
11
+ Type.String({
12
+ description: `File caption in markdown. Max ${MAX_CAPTION_CHARS} chars.`,
13
+ maxLength: MAX_CAPTION_CHARS,
14
+ })
15
+ ),
16
+ });
17
+
18
+ export type SendFileInput = Static<typeof sendFileSchema>;
@@ -0,0 +1,65 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { InputFile, type Api } from "grammy";
3
+ import { basename } from "node:path";
4
+
5
+ import { SendFile } from "#core/shared/SendFile";
6
+ import type { PimToolDefinition } from "#core/shared/Tools";
7
+ import type { ToolView } from "#core/view/ViewBlock";
8
+ import {
9
+ MAX_CAPTION_CHARS,
10
+ sendFileSchema,
11
+ type SendFileInput,
12
+ } from "./SendFileSchema";
13
+ import type { SessionId } from "./Session";
14
+
15
+ export type SendFileDeps = {
16
+ readonly api: Api;
17
+ readonly sessionId: SessionId;
18
+ readonly cwd: string;
19
+ };
20
+
21
+ function build(deps: SendFileDeps): PimToolDefinition<typeof sendFileSchema> {
22
+ return {
23
+ ...defineTool({
24
+ name: "send_file",
25
+ label: "send_file",
26
+ description: `Send a local file to the current Telegram chat/thread as a document. Max ${SendFile.MAX_BYTES / (1024 * 1024)} MB.`,
27
+ parameters: sendFileSchema,
28
+ async execute(_id, params) {
29
+ const { path: rawPath, caption } = params as SendFileInput;
30
+ const resolved = await SendFile.validate(rawPath, deps.cwd);
31
+ const trimmedCaption = caption?.slice(0, MAX_CAPTION_CHARS);
32
+ await send(deps.api, deps.sessionId, resolved.path, trimmedCaption);
33
+ return {
34
+ content: [{ type: "text", text: `Sent ${basename(resolved.path)}` }],
35
+ details: {
36
+ path: resolved.path,
37
+ bytes: resolved.size,
38
+ },
39
+ };
40
+ },
41
+ }),
42
+ toViewModel: ({ args }): ToolView => {
43
+ const path = (args as Partial<SendFileInput> | undefined)?.path;
44
+ return {
45
+ label: "Send File",
46
+ icon: "upload",
47
+ title: [{ kind: "file", path: path ?? "..." }],
48
+ };
49
+ },
50
+ };
51
+ }
52
+
53
+ async function send(
54
+ api: Api,
55
+ sessionId: SessionId,
56
+ path: string,
57
+ caption: string | undefined
58
+ ): Promise<void> {
59
+ await api.sendDocument(sessionId.chatId, new InputFile(path), {
60
+ message_thread_id: sessionId.threadId,
61
+ caption,
62
+ });
63
+ }
64
+
65
+ export const SendFileTool = { build };