@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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 (274) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/SECURITY.md +5 -0
  5. package/package.json +112 -4
  6. package/packages/board/CHANGELOG.md +340 -0
  7. package/packages/board/CONTRIBUTING.md +7 -0
  8. package/packages/board/LICENSE +21 -0
  9. package/packages/board/README.md +88 -0
  10. package/packages/board/SECURITY.md +19 -0
  11. package/packages/board/docs/how-it-works.md +31 -0
  12. package/packages/board/docs/reference.md +278 -0
  13. package/packages/board/package.json +81 -0
  14. package/packages/board/runner/job-runner-legacy.mjs +68 -0
  15. package/packages/board/runner/job-runner.mjs +834 -0
  16. package/packages/board/runner/pty-runner-legacy.mjs +50 -0
  17. package/packages/board/runner/pty-runner.mjs +2175 -0
  18. package/packages/board/runner/state-coordinator.mjs +673 -0
  19. package/packages/board/runner/state-runner.mjs +203 -0
  20. package/packages/board/runner/title-runner.mjs +80 -0
  21. package/packages/board/src/commands/agent-board.ts +394 -0
  22. package/packages/board/src/commands/attach-decision.mjs +66 -0
  23. package/packages/board/src/commands/attach-flow.ts +251 -0
  24. package/packages/board/src/commands/bg.ts +103 -0
  25. package/packages/board/src/commands/board-model.ts +21 -0
  26. package/packages/board/src/core/atomic.mjs +184 -0
  27. package/packages/board/src/core/attach-delete.mjs +24 -0
  28. package/packages/board/src/core/attach-frame-proof.mjs +870 -0
  29. package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
  30. package/packages/board/src/core/attach-frame-stream.mjs +266 -0
  31. package/packages/board/src/core/attach-view-proof.mjs +709 -0
  32. package/packages/board/src/core/auto-state.mjs +377 -0
  33. package/packages/board/src/core/background-runs.mjs +273 -0
  34. package/packages/board/src/core/code-refs-store.mjs +315 -0
  35. package/packages/board/src/core/code-refs.mjs +913 -0
  36. package/packages/board/src/core/config.mjs +77 -0
  37. package/packages/board/src/core/control-protocol.mjs +482 -0
  38. package/packages/board/src/core/coordinator-client.mjs +633 -0
  39. package/packages/board/src/core/coordinator-journal.mjs +282 -0
  40. package/packages/board/src/core/coordinator-protocol.mjs +14 -0
  41. package/packages/board/src/core/cwd-stats.mjs +114 -0
  42. package/packages/board/src/core/dashboard-render.mjs +10 -0
  43. package/packages/board/src/core/derive.mjs +117 -0
  44. package/packages/board/src/core/diagnostics.mjs +133 -0
  45. package/packages/board/src/core/double-press.mjs +26 -0
  46. package/packages/board/src/core/editor-state-reporter.mjs +112 -0
  47. package/packages/board/src/core/events.mjs +286 -0
  48. package/packages/board/src/core/evidence.mjs +245 -0
  49. package/packages/board/src/core/follow-up-queue.mjs +211 -0
  50. package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
  51. package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
  52. package/packages/board/src/core/frame-witness-update.mjs +35 -0
  53. package/packages/board/src/core/heuristics.mjs +313 -0
  54. package/packages/board/src/core/hold-input.mjs +27 -0
  55. package/packages/board/src/core/host-coordination.mjs +300 -0
  56. package/packages/board/src/core/host-crash.mjs +79 -0
  57. package/packages/board/src/core/host-probe.mjs +207 -0
  58. package/packages/board/src/core/host-protocol.mjs +73 -0
  59. package/packages/board/src/core/ids.mjs +35 -0
  60. package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
  61. package/packages/board/src/core/invocation.mjs +43 -0
  62. package/packages/board/src/core/launch-options.mjs +418 -0
  63. package/packages/board/src/core/launch.mjs +157 -0
  64. package/packages/board/src/core/load-config.mjs +4 -0
  65. package/packages/board/src/core/locks.mjs +530 -0
  66. package/packages/board/src/core/paths.mjs +183 -0
  67. package/packages/board/src/core/pid.mjs +75 -0
  68. package/packages/board/src/core/prewarm-schedule.mjs +41 -0
  69. package/packages/board/src/core/process-identity-probe.mjs +228 -0
  70. package/packages/board/src/core/prompt-transport.mjs +13 -0
  71. package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
  72. package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
  73. package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
  74. package/packages/board/src/core/pty-attach-render.mjs +229 -0
  75. package/packages/board/src/core/pty-links.mjs +81 -0
  76. package/packages/board/src/core/pty-scroll.mjs +522 -0
  77. package/packages/board/src/core/pty-support.mjs +327 -0
  78. package/packages/board/src/core/repo.mjs +103 -0
  79. package/packages/board/src/core/rows.mjs +414 -0
  80. package/packages/board/src/core/screen-log-gc.mjs +198 -0
  81. package/packages/board/src/core/screen-log.mjs +160 -0
  82. package/packages/board/src/core/session-view.mjs +174 -0
  83. package/packages/board/src/core/slash-commands.mjs +52 -0
  84. package/packages/board/src/core/state-commands.mjs +757 -0
  85. package/packages/board/src/core/status-consistency.mjs +98 -0
  86. package/packages/board/src/core/steering-prompts.mjs +34 -0
  87. package/packages/board/src/core/steering.mjs +133 -0
  88. package/packages/board/src/core/stop-latch.mjs +41 -0
  89. package/packages/board/src/core/store.mjs +523 -0
  90. package/packages/board/src/core/suspend.mjs +36 -0
  91. package/packages/board/src/core/terminal-attach-client.mjs +981 -0
  92. package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
  93. package/packages/board/src/core/terminal-model.mjs +222 -0
  94. package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
  95. package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
  96. package/packages/board/src/core/terminal-snapshot.mjs +440 -0
  97. package/packages/board/src/core/test-knobs.mjs +28 -0
  98. package/packages/board/src/core/title.mjs +43 -0
  99. package/packages/board/src/core/types.mjs +414 -0
  100. package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
  101. package/packages/board/src/core/worktree.mjs +65 -0
  102. package/packages/board/src/index.ts +303 -0
  103. package/packages/board/src/runtime/service.mjs +3222 -0
  104. package/packages/board/src/ui/chrome.ts +20 -0
  105. package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
  106. package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
  107. package/packages/board/src/ui/dashboard.ts +2237 -0
  108. package/packages/board/src/ui/double-left.ts +30 -0
  109. package/packages/board/src/ui/external-editor.ts +25 -0
  110. package/packages/board/src/ui/focus.ts +32 -0
  111. package/packages/board/src/ui/pty-attach.ts +3184 -0
  112. package/packages/board/src/ui/waiting.ts +30 -0
  113. package/packages/commands/LICENSE +21 -0
  114. package/packages/commands/README.md +19 -0
  115. package/packages/commands/package.json +22 -0
  116. package/packages/commands/src/index.ts +43 -0
  117. package/packages/commands/src/security-review.ts +109 -0
  118. package/packages/frame/CHANGELOG.md +113 -0
  119. package/packages/frame/CONTRIBUTING.md +7 -0
  120. package/packages/frame/LICENSE +22 -0
  121. package/packages/frame/README.md +105 -0
  122. package/packages/frame/SECURITY.md +16 -0
  123. package/packages/frame/docs/how-it-works.md +53 -0
  124. package/packages/frame/docs/reference.md +78 -0
  125. package/packages/frame/package.json +72 -0
  126. package/packages/frame/src/data/git.ts +90 -0
  127. package/packages/frame/src/data/github.ts +140 -0
  128. package/packages/frame/src/data/gitlab.ts +63 -0
  129. package/packages/frame/src/data/mode.ts +49 -0
  130. package/packages/frame/src/data/past-prompts.ts +57 -0
  131. package/packages/frame/src/data/runner.ts +32 -0
  132. package/packages/frame/src/data/snapshot.ts +223 -0
  133. package/packages/frame/src/data/start-notes.ts +104 -0
  134. package/packages/frame/src/data/telemetry.ts +337 -0
  135. package/packages/frame/src/data/todos.ts +162 -0
  136. package/packages/frame/src/data/totals.ts +94 -0
  137. package/packages/frame/src/data/visits.ts +45 -0
  138. package/packages/frame/src/index.ts +696 -0
  139. package/packages/frame/src/kit/LICENSE +21 -0
  140. package/packages/frame/src/kit/app/attribution.ts +30 -0
  141. package/packages/frame/src/kit/app/builder.ts +186 -0
  142. package/packages/frame/src/kit/app/claim.ts +27 -0
  143. package/packages/frame/src/kit/app/feature.ts +91 -0
  144. package/packages/frame/src/kit/app/host.ts +214 -0
  145. package/packages/frame/src/kit/app/renderers.ts +62 -0
  146. package/packages/frame/src/kit/app/scope.ts +125 -0
  147. package/packages/frame/src/kit/board/model.ts +337 -0
  148. package/packages/frame/src/kit/contract.ts +79 -0
  149. package/packages/frame/src/kit/contracts/accounts.ts +33 -0
  150. package/packages/frame/src/kit/contracts/ask.ts +120 -0
  151. package/packages/frame/src/kit/contracts/board.ts +6 -0
  152. package/packages/frame/src/kit/contracts/by.ts +22 -0
  153. package/packages/frame/src/kit/contracts/calls.ts +46 -0
  154. package/packages/frame/src/kit/contracts/events.ts +89 -0
  155. package/packages/frame/src/kit/contracts/mode.ts +26 -0
  156. package/packages/frame/src/kit/contracts/names.ts +12 -0
  157. package/packages/frame/src/kit/contracts/peers.ts +77 -0
  158. package/packages/frame/src/kit/contracts/prompts.ts +8 -0
  159. package/packages/frame/src/kit/contracts/screen.ts +75 -0
  160. package/packages/frame/src/kit/contracts/skills.ts +81 -0
  161. package/packages/frame/src/kit/contracts/sources.ts +81 -0
  162. package/packages/frame/src/kit/contracts/views.ts +42 -0
  163. package/packages/frame/src/kit/control.ts +59 -0
  164. package/packages/frame/src/kit/dark-theme.ts +41 -0
  165. package/packages/frame/src/kit/decode.ts +210 -0
  166. package/packages/frame/src/kit/events/log.ts +313 -0
  167. package/packages/frame/src/kit/format.ts +25 -0
  168. package/packages/frame/src/kit/host.ts +4 -0
  169. package/packages/frame/src/kit/index.ts +25 -0
  170. package/packages/frame/src/kit/legacy-tasks.ts +39 -0
  171. package/packages/frame/src/kit/overlay-images.ts +36 -0
  172. package/packages/frame/src/kit/paths.ts +16 -0
  173. package/packages/frame/src/kit/screen/client.ts +74 -0
  174. package/packages/frame/src/kit/screen/feature.ts +64 -0
  175. package/packages/frame/src/kit/screen/model.ts +182 -0
  176. package/packages/frame/src/kit/screen/view.ts +838 -0
  177. package/packages/frame/src/kit/settings/files.ts +90 -0
  178. package/packages/frame/src/kit/settings/setting.ts +57 -0
  179. package/packages/frame/src/kit/settings/store.ts +192 -0
  180. package/packages/frame/src/kit/sources/file.ts +129 -0
  181. package/packages/frame/src/kit/sources/follow.ts +155 -0
  182. package/packages/frame/src/kit/testing.ts +260 -0
  183. package/packages/frame/src/kit/totals.ts +72 -0
  184. package/packages/frame/src/kit/ui/entry.ts +75 -0
  185. package/packages/frame/src/kit/ui/frame.ts +212 -0
  186. package/packages/frame/src/kit/ui/glyphs.ts +132 -0
  187. package/packages/frame/src/kit/ui/scroll.ts +14 -0
  188. package/packages/frame/src/render/diff.ts +157 -0
  189. package/packages/frame/src/render/fit.ts +86 -0
  190. package/packages/frame/src/render/format.ts +108 -0
  191. package/packages/frame/src/render/frame.ts +106 -0
  192. package/packages/frame/src/render/glyphs.ts +189 -0
  193. package/packages/frame/src/render/keys.ts +43 -0
  194. package/packages/frame/src/render/lead.ts +37 -0
  195. package/packages/frame/src/render/paint.ts +135 -0
  196. package/packages/frame/src/render/safe.ts +19 -0
  197. package/packages/frame/src/render/segments.ts +735 -0
  198. package/packages/frame/src/render/streaming.ts +223 -0
  199. package/packages/frame/src/render/timing.ts +106 -0
  200. package/packages/frame/src/render/tools.ts +167 -0
  201. package/packages/frame/src/settings.ts +105 -0
  202. package/packages/frame/src/ui/editor.ts +272 -0
  203. package/packages/frame/src/ui/effort.ts +36 -0
  204. package/packages/frame/src/ui/footer.ts +165 -0
  205. package/packages/frame/src/ui/header.ts +362 -0
  206. package/packages/frame/src/ui/history-search.ts +23 -0
  207. package/packages/frame/src/ui/interrupt.ts +21 -0
  208. package/packages/frame/src/ui/outputs.ts +170 -0
  209. package/packages/frame/src/ui/pi-entries.ts +298 -0
  210. package/packages/frame/src/ui/previews.ts +72 -0
  211. package/packages/frame/src/ui/queue.ts +75 -0
  212. package/packages/frame/src/ui/screen.ts +59 -0
  213. package/packages/frame/src/ui/timings.ts +48 -0
  214. package/packages/frame/src/ui/todos.ts +205 -0
  215. package/packages/frame/src/ui/tool-frame.ts +595 -0
  216. package/packages/frame/src/ui/working.ts +25 -0
  217. package/packages/frame/src/view/state.ts +97 -0
  218. package/packages/frame/src/view/transcript.ts +175 -0
  219. package/packages/frame/src/view/view.ts +162 -0
  220. package/packages/memory/CONTRIBUTING.md +7 -0
  221. package/packages/memory/LICENSE +21 -0
  222. package/packages/memory/README.md +44 -0
  223. package/packages/memory/SECURITY.md +20 -0
  224. package/packages/memory/package.json +55 -0
  225. package/packages/memory/skills/review-memory/SKILL.md +48 -0
  226. package/packages/memory/src/index.ts +163 -0
  227. package/packages/memory/src/prompt.ts +25 -0
  228. package/packages/memory/src/review.ts +150 -0
  229. package/packages/memory/src/secrets.ts +22 -0
  230. package/packages/memory/src/store.ts +173 -0
  231. package/packages/modes/CHANGELOG.md +104 -0
  232. package/packages/modes/CONTRIBUTING.md +7 -0
  233. package/packages/modes/LICENSE +21 -0
  234. package/packages/modes/README.md +174 -0
  235. package/packages/modes/SECURITY.md +17 -0
  236. package/packages/modes/package.json +55 -0
  237. package/packages/modes/skills/review-permissions/SKILL.md +54 -0
  238. package/packages/modes/src/bash.ts +218 -0
  239. package/packages/modes/src/deny-off.ts +62 -0
  240. package/packages/modes/src/git-safety.ts +72 -0
  241. package/packages/modes/src/index.ts +338 -0
  242. package/packages/modes/src/interview.ts +206 -0
  243. package/packages/modes/src/judge-gate.ts +113 -0
  244. package/packages/modes/src/modes.ts +121 -0
  245. package/packages/modes/src/review/apply.mjs +156 -0
  246. package/packages/modes/src/review/collect.mjs +208 -0
  247. package/packages/modes/src/review/tools.ts +71 -0
  248. package/packages/modes/src/sandbox.ts +37 -0
  249. package/packages/router/CHANGELOG.md +8 -0
  250. package/packages/router/LICENSE +21 -0
  251. package/packages/router/README.md +90 -0
  252. package/packages/router/package.json +51 -0
  253. package/packages/router/src/config.ts +65 -0
  254. package/packages/router/src/index.ts +172 -0
  255. package/packages/router/src/judge.ts +51 -0
  256. package/packages/router/src/route.ts +67 -0
  257. package/packages/web/CHANGELOG.md +36 -0
  258. package/packages/web/LICENSE +21 -0
  259. package/packages/web/README.md +87 -0
  260. package/packages/web/SECURITY.md +13 -0
  261. package/packages/web/package.json +59 -0
  262. package/packages/web/src/config.ts +109 -0
  263. package/packages/web/src/dnsfilter.ts +97 -0
  264. package/packages/web/src/extract.ts +65 -0
  265. package/packages/web/src/index.ts +217 -0
  266. package/packages/web/src/markdown.ts +122 -0
  267. package/packages/web/src/net.ts +109 -0
  268. package/packages/web/src/reader.ts +73 -0
  269. package/packages/web/src/sanitize.ts +50 -0
  270. package/packages/web/src/search.ts +76 -0
  271. package/packages/web/src/secrets.ts +22 -0
  272. package/packages/web/src/validate.ts +186 -0
  273. package/src/index.ts +156 -0
  274. package/src/settings.ts +79 -0
@@ -0,0 +1,223 @@
1
+ import { hostname } from "node:os";
2
+
3
+ import type { AccountState, Board, PeerMark, RoomMark, WritingMark } from "../kit/index.ts";
4
+ import { VERSION, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
5
+
6
+ import { plainText } from "../render/safe.ts";
7
+
8
+ import type { GitState } from "./git.ts";
9
+ import type { RequestView, RunView } from "./telemetry.ts";
10
+ import {
11
+ type Averages,
12
+ averagesOf,
13
+ cacheHitPercent,
14
+ cacheWarming,
15
+ emptyTotals,
16
+ type Totals,
17
+ totalsOf,
18
+ } from "./totals.ts";
19
+
20
+ interface ModelInfo {
21
+ name: string;
22
+ provider: string;
23
+ reasoning: boolean;
24
+ }
25
+
26
+ interface ContextInfo {
27
+ percent: number;
28
+ tokens: number;
29
+ window: number;
30
+ }
31
+
32
+ /** pi-sandbox sets this while it's on, and empties it when it goes off. */
33
+ export const SANDBOX_STATUS = "sandbox";
34
+
35
+ export interface Snapshot {
36
+ now: number;
37
+ cwd: string;
38
+ home: string | undefined;
39
+ branch: string | null;
40
+ git: GitState | undefined;
41
+ host: string | undefined;
42
+ sessionName: string | undefined;
43
+ /** The word the room calls this session by, in its color once it is in a room. */
44
+ me: { handle: string; color?: string } | undefined;
45
+ version: string;
46
+ model: ModelInfo | undefined;
47
+ account: AccountState | undefined;
48
+ thinking: string | undefined;
49
+ context: ContextInfo | undefined;
50
+ totals: Totals;
51
+ cacheHit: number | undefined;
52
+ cacheWarming: boolean;
53
+ subscription: boolean;
54
+ request: RequestView | undefined;
55
+ last: RequestView | undefined;
56
+ run: RunView | undefined;
57
+ averages: Averages;
58
+ statuses: ReadonlyMap<string, string>;
59
+ /** Whether pi-sandbox is confining commands. Undefined when it isn't installed. */
60
+ sandbox: "on" | "off" | undefined;
61
+ /** When a board feature keeps it. */
62
+ board: Board | undefined;
63
+ /** When a peers feature keeps them. */
64
+ peers: PeerMark[];
65
+ /** Empty when the peers feature publishes only `peers`. */
66
+ peersRooms: RoomMark[];
67
+ peersUnread: number;
68
+ peersWriting: WritingMark[];
69
+ }
70
+
71
+ export function emptySnapshot(): Snapshot {
72
+ return {
73
+ now: 0,
74
+ cwd: "",
75
+ home: undefined,
76
+ branch: null,
77
+ git: undefined,
78
+ host: undefined,
79
+ sessionName: undefined,
80
+ me: undefined,
81
+ version: VERSION,
82
+ model: undefined,
83
+ account: undefined,
84
+ thinking: undefined,
85
+ context: undefined,
86
+ totals: emptyTotals(),
87
+ cacheHit: undefined,
88
+ cacheWarming: false,
89
+ subscription: false,
90
+ request: undefined,
91
+ last: undefined,
92
+ run: undefined,
93
+ averages: { decode: undefined, prefill: undefined },
94
+ statuses: new Map(),
95
+ sandbox: undefined,
96
+ board: undefined,
97
+ peers: [],
98
+ peersRooms: [],
99
+ peersUnread: 0,
100
+ peersWriting: [],
101
+ };
102
+ }
103
+
104
+ export interface LiveInputs {
105
+ branch: string | null;
106
+ git: GitState | undefined;
107
+ account?: AccountState | undefined;
108
+ request: RequestView | undefined;
109
+ last: RequestView | undefined;
110
+ run: RunView | undefined;
111
+ statuses: ReadonlyMap<string, string>;
112
+ sandboxInstalled?: boolean;
113
+ board?: Board | undefined;
114
+ me?: { handle: string; color?: string } | undefined;
115
+ peers?: PeerMark[] | undefined;
116
+ peersRooms?: RoomMark[] | undefined;
117
+ peersUnread?: number | undefined;
118
+ peersWriting?: WritingMark[] | undefined;
119
+ }
120
+
121
+ interface Derived {
122
+ totals: Totals;
123
+ cacheHit: number | undefined;
124
+ cacheWarming: boolean;
125
+ averages: Averages;
126
+ context: ContextInfo | undefined;
127
+ answeredBy: string | undefined;
128
+ }
129
+
130
+ const VIRTUAL_API = "pi-virtual";
131
+
132
+ export class SnapshotReader {
133
+ #key: string | undefined;
134
+ #derived: Derived | undefined;
135
+ readonly #host = hostname().split(".")[0];
136
+
137
+ read(ctx: ExtensionContext, live: LiveInputs): Snapshot {
138
+ const model = ctx.model;
139
+ const derived = this.#derive(ctx);
140
+
141
+ return {
142
+ now: Date.now(),
143
+ cwd: ctx.cwd,
144
+ home: process.env.HOME ?? process.env.USERPROFILE,
145
+ branch: live.branch,
146
+ git: live.git,
147
+ host: this.#host,
148
+ sessionName: ctx.sessionManager.getSessionName(),
149
+ me: live.me,
150
+ version: VERSION,
151
+ model:
152
+ model === undefined
153
+ ? undefined
154
+ : {
155
+ name:
156
+ model.api === VIRTUAL_API && derived.answeredBy !== undefined
157
+ ? `${model.name} \u2192 ${derived.answeredBy}`
158
+ : (model.name ?? model.id),
159
+ provider: ctx.modelRegistry.getProviderDisplayName(model.provider),
160
+ reasoning: model.reasoning,
161
+ },
162
+ thinking: ctx.thinkingLevel,
163
+ account: live.account,
164
+ ...derived,
165
+ subscription: model === undefined ? false : ctx.modelRegistry.isUsingOAuth(model),
166
+ request: live.request,
167
+ last: live.last,
168
+ run: live.run,
169
+ statuses: new Map(
170
+ [...live.statuses.entries()]
171
+ .toSorted(([a], [b]) => a.localeCompare(b))
172
+ .map(([key, text]): [string, string] => [key, sanitize(text)])
173
+ .filter(([, text]) => text !== ""),
174
+ ),
175
+ sandbox: !live.sandboxInstalled ? undefined : live.statuses.get(SANDBOX_STATUS) ? "on" : "off",
176
+ board: live.board,
177
+ peers: live.peers ?? [],
178
+ peersRooms: live.peersRooms ?? [],
179
+ peersUnread: live.peersUnread ?? 0,
180
+ peersWriting: live.peersWriting ?? [],
181
+ };
182
+ }
183
+
184
+ #derive(ctx: ExtensionContext): Derived {
185
+ // Entries are appended at the leaf, so the sums change only when the leaf or the model does.
186
+ const key = `${ctx.sessionManager.getLeafId() ?? ""}\u0000${ctx.model?.provider}/${ctx.model?.id}`;
187
+ if (this.#derived !== undefined && key === this.#key) return this.#derived;
188
+
189
+ const entries: readonly SessionEntry[] = ctx.sessionManager.getBranch();
190
+ this.#key = key;
191
+ this.#derived = {
192
+ totals: totalsOf(entries),
193
+ cacheHit: cacheHitPercent(entries),
194
+ cacheWarming: cacheWarming(entries),
195
+ averages: averagesOf(entries),
196
+ context: contextOf(ctx),
197
+ answeredBy: answeredBy(ctx, entries),
198
+ };
199
+ return this.#derived;
200
+ }
201
+ }
202
+
203
+ /** A virtual model only says what was selected. The latest answer says which model it chose. */
204
+ function answeredBy(ctx: ExtensionContext, entries: readonly SessionEntry[]): string | undefined {
205
+ for (let index = entries.length - 1; index >= 0; index--) {
206
+ const entry = entries[index];
207
+ if (entry?.type !== "message" || entry.message.role !== "assistant") continue;
208
+ if (entry.message.stopReason === "error" || entry.message.stopReason === "aborted") continue;
209
+ const { provider, model } = entry.message;
210
+ return plainText(ctx.modelRegistry.find(provider, model)?.name ?? model).replace(/\s+/g, " ");
211
+ }
212
+ return undefined;
213
+ }
214
+
215
+ function contextOf(ctx: ExtensionContext): ContextInfo | undefined {
216
+ const usage = ctx.getContextUsage();
217
+ if (usage === undefined || usage.percent === null || usage.tokens === null) return undefined;
218
+ return { percent: usage.percent, tokens: usage.tokens, window: usage.contextWindow };
219
+ }
220
+
221
+ function sanitize(text: string): string {
222
+ return text.replace(/[\r\n\t]+/g, " ").trim();
223
+ }
@@ -0,0 +1,104 @@
1
+ // Under the folder and the model, only what changed or waits on you since the last session here.
2
+ import { formatClock, GLYPH } from "../kit/index.ts";
3
+
4
+ import type { GithubState } from "./github.ts";
5
+ import type { Snapshot } from "./snapshot.ts";
6
+
7
+ const PLAN_LIMIT = 70;
8
+
9
+ export interface LastVisit {
10
+ at: number;
11
+ version: string;
12
+ }
13
+
14
+ export interface StartNote {
15
+ mark: string;
16
+ color: string;
17
+ text: string;
18
+ detail?: string;
19
+ }
20
+
21
+ export function startNotes(
22
+ snapshot: Pick<Snapshot, "peers" | "account" | "git" | "branch">,
23
+ version: string,
24
+ last: LastVisit | undefined,
25
+ github?: GithubState,
26
+ ): StartNote[] {
27
+ const notes: StartNote[] = [];
28
+ const { account, git } = snapshot;
29
+
30
+ if (account?.needsLogin === true)
31
+ notes.push({
32
+ mark: GLYPH.cross,
33
+ color: "error",
34
+ text: `${account.label} needs a sign-in`,
35
+ detail: "/accounts",
36
+ });
37
+ if (git !== undefined && git.conflicted > 0)
38
+ notes.push({ mark: GLYPH.cross, color: "error", text: `${git.conflicted} files in conflict` });
39
+ if (git !== undefined && git.behind > 0)
40
+ notes.push({
41
+ mark: GLYPH.down,
42
+ color: "warning",
43
+ text: `${snapshot.branch ?? "this branch"} is ${git.behind} behind`,
44
+ detail: "pull before you start",
45
+ });
46
+ if (github?.ci !== undefined)
47
+ notes.push({
48
+ mark: GLYPH.cross,
49
+ color: "error",
50
+ text: `CI failed on ${github.ci.branch}`,
51
+ detail: `${github.ci.sha} ${github.ci.name}`,
52
+ });
53
+ if (github?.pr !== undefined)
54
+ notes.push({
55
+ mark: GLYPH.flag,
56
+ color: "warning",
57
+ text: `${github.pr.ref ?? `PR #${github.pr.number}`} ${github.pr.title}`,
58
+ detail: github.pr.waits,
59
+ });
60
+ for (const peer of snapshot.peers)
61
+ notes.push({
62
+ mark: GLYPH.diamond,
63
+ color: peer.color,
64
+ text: `@${peer.handle}`,
65
+ detail: peerState(peer.state),
66
+ });
67
+ for (const window of account?.windows ?? []) {
68
+ if (window.used < PLAN_LIMIT) continue;
69
+ notes.push({
70
+ mark: GLYPH.up,
71
+ color: window.used >= 90 ? "error" : "warning",
72
+ text: `${window.name} at ${Math.round(window.used)}%`,
73
+ detail: window.resetsIn === undefined ? undefined : `resets in ${window.resetsIn}`,
74
+ });
75
+ }
76
+ if (last !== undefined && last.version !== version)
77
+ notes.push({
78
+ mark: GLYPH.up,
79
+ color: "accent",
80
+ text: `${GLYPH.pi}-frame ${version} is new`,
81
+ detail: `was ${last.version}`,
82
+ });
83
+ return notes;
84
+ }
85
+
86
+ function peerState(state: "running" | "idle" | "waiting"): string {
87
+ switch (state) {
88
+ case "running":
89
+ return "works here";
90
+ case "waiting":
91
+ return "waits for you";
92
+ case "idle":
93
+ return "is open here";
94
+ }
95
+ }
96
+
97
+ /** Local time, or the day when it was not today. */
98
+ export function since(at: number, now: number): string {
99
+ const then = new Date(at);
100
+ const time = formatClock(at);
101
+ if (new Date(now).toDateString() === then.toDateString()) return time;
102
+ const days = Math.round((now - at) / 86_400_000);
103
+ return days <= 1 ? `yesterday ${time}` : `${days} days ago`;
104
+ }
@@ -0,0 +1,337 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { STALL_MS } from "../render/timing.ts";
4
+ import {
5
+ emptyTotals,
6
+ perSecond,
7
+ promptOf,
8
+ REQUEST_CHANGES,
9
+ type RequestRecord,
10
+ type Totals,
11
+ usageTotals,
12
+ } from "./totals.ts";
13
+
14
+ const MIN_SAMPLE_MS = 250;
15
+ /** Measured over 16,889 assistant messages: 2.88 characters per output token. */
16
+ const DEFAULT_CHARS_PER_TOKEN = 2.9;
17
+
18
+ /** So the wait for the first thought can be told from the first word. */
19
+ export type DeltaKind = "thinking" | "writing";
20
+
21
+ export interface RequestView {
22
+ streaming: boolean;
23
+ waiting: boolean;
24
+ elapsedMs: number;
25
+ /** From the request leaving to the first token of any kind. */
26
+ waitMs: number | undefined;
27
+ /** The same wait while it runs. */
28
+ waitingMs: number;
29
+ /** To the response headers: the provider's own latency. */
30
+ serverMs: number | undefined;
31
+ /** From the headers to the first token: the model reading the prompt. */
32
+ prefillMs: number | undefined;
33
+ /** Unset when it wrote without thinking. */
34
+ thoughtMs: number | undefined;
35
+ decode: number | undefined;
36
+ prefill: number | undefined;
37
+ usage: Totals;
38
+ estimated: boolean;
39
+ }
40
+
41
+ /** One call to the model, for the line under its response. */
42
+ export interface Timing {
43
+ /** When it left. */
44
+ at: number;
45
+ streaming: boolean;
46
+ waitMs: number | undefined;
47
+ waitingMs: number;
48
+ serverMs: number | undefined;
49
+ thoughtMs: number | undefined;
50
+ totalMs: number;
51
+ writingMs: number;
52
+ /** Pauses of `STALL_MS` or more while it wrote. */
53
+ stallMs: number;
54
+ output: number;
55
+ estimated: boolean;
56
+ }
57
+
58
+ export interface RunView {
59
+ running: boolean;
60
+ elapsedMs: number;
61
+ requests: number;
62
+ cost: number;
63
+ }
64
+
65
+ interface Request {
66
+ startedAt: number;
67
+ sentAt: number | undefined;
68
+ headersAt: number | undefined;
69
+ firstTokenAt: number | undefined;
70
+ firstWritingAt: number | undefined;
71
+ endedAt: number | undefined;
72
+ messageAt?: number;
73
+ usage: Totals;
74
+ characters: number;
75
+ lastDeltaAt: number | undefined;
76
+ stallMs: number;
77
+ }
78
+
79
+ interface Run {
80
+ startedAt: number;
81
+ endedAt: number | undefined;
82
+ requests: number;
83
+ cost: number;
84
+ }
85
+
86
+ interface UsageLike {
87
+ input: number;
88
+ output: number;
89
+ cacheRead: number;
90
+ cacheWrite: number;
91
+ cost: { total: number };
92
+ }
93
+
94
+ function span(from: number | undefined, to: number | undefined): number | undefined {
95
+ return from === undefined || to === undefined ? undefined : to - from;
96
+ }
97
+
98
+ export class Telemetry {
99
+ readonly #now: () => number;
100
+ #run: Run | undefined;
101
+ #request: Request | undefined;
102
+ #last: RequestView | undefined;
103
+ /** By the response's timestamp, which stays the same while it's written. */
104
+ #byMessage = new Map<number, Request>();
105
+ #restored = new Map<number, Timing>();
106
+ /** They arrive before the pi message they belong to. */
107
+ #sentAt: number | undefined;
108
+ #headersAt: number | undefined;
109
+ /** So the live estimate uses a real ratio. */
110
+ #sampled = { characters: 0, output: 0 };
111
+
112
+ constructor(now: () => number = Date.now) {
113
+ this.#now = now;
114
+ }
115
+
116
+ runStarted(): void {
117
+ this.#run = { startedAt: this.#now(), endedAt: undefined, requests: 0, cost: 0 };
118
+ }
119
+
120
+ runEnded(): void {
121
+ if (this.#run !== undefined && this.#run.endedAt === undefined) this.#run.endedAt = this.#now();
122
+ }
123
+
124
+ /** Before every call to the model: the start of the wait. */
125
+ providerRequestSent(): void {
126
+ this.#sentAt = this.#now();
127
+ this.#headersAt = undefined;
128
+ this.#attach();
129
+ }
130
+
131
+ /** Before the stream is read. */
132
+ providerResponseArrived(): void {
133
+ this.#headersAt = this.#now();
134
+ this.#attach();
135
+ }
136
+
137
+ requestStarted(): void {
138
+ this.#sentAt = undefined;
139
+ this.#headersAt = undefined;
140
+ this.#begin();
141
+ if (this.#run === undefined || this.#run.endedAt !== undefined) this.runStarted();
142
+ if (this.#run !== undefined) this.#run.requests++;
143
+ }
144
+
145
+ /** Later requests of a turn: the next call after a tool ran. */
146
+ answerStarted(messageAt?: number): void {
147
+ if (this.#request === undefined || this.#request.endedAt !== undefined) this.#begin();
148
+ if (messageAt !== undefined && this.#request !== undefined) {
149
+ this.#byMessage.set(messageAt, this.#request);
150
+ this.#request.messageAt = messageAt;
151
+ }
152
+ }
153
+
154
+ answerGrew(delta: string, usage: UsageLike, kind: DeltaKind): void {
155
+ const request = this.#request;
156
+ if (request === undefined || request.endedAt !== undefined) return;
157
+ request.usage = usageTotals(usage);
158
+ if (delta.length === 0) return;
159
+ const grewAt = this.#now();
160
+ const gap = grewAt - (request.lastDeltaAt ?? grewAt);
161
+ if (gap >= STALL_MS) request.stallMs += gap;
162
+ request.lastDeltaAt = grewAt;
163
+ request.characters += delta.length;
164
+ request.firstTokenAt ??= this.#now();
165
+ if (kind === "writing") request.firstWritingAt ??= this.#now();
166
+ }
167
+
168
+ answerEnded(usage: UsageLike): RequestRecord | undefined {
169
+ const request = this.#request;
170
+ if (request === undefined || request.endedAt !== undefined) return undefined;
171
+
172
+ const now = this.#now();
173
+ request.endedAt = now;
174
+ request.usage = usageTotals(usage);
175
+ if (this.#run !== undefined) this.#run.cost += usage.cost.total;
176
+ if (request.usage.output === 0 && request.firstTokenAt === undefined) return undefined;
177
+
178
+ if (request.characters > 0 && request.usage.output > 0) {
179
+ this.#sampled.characters += request.characters;
180
+ this.#sampled.output += request.usage.output;
181
+ }
182
+
183
+ const firstTokenAt = request.firstTokenAt ?? now;
184
+ const from = request.sentAt ?? request.startedAt;
185
+ this.#last = this.#view(request);
186
+ const timing = this.#timing(request);
187
+ return {
188
+ output: request.usage.output,
189
+ prompt: promptOf(request.usage),
190
+ firstTokenMs: firstTokenAt - from,
191
+ generationMs: now - firstTokenAt,
192
+ at: from,
193
+ messageAt: request.messageAt,
194
+ serverMs: span(request.sentAt, request.headersAt),
195
+ thoughtMs: timing.thoughtMs,
196
+ totalMs: now - from,
197
+ stallMs: request.stallMs,
198
+ };
199
+ }
200
+
201
+ timingOf(messageAt: number): Timing | undefined {
202
+ const request = this.#byMessage.get(messageAt);
203
+ return request === undefined ? this.#restored.get(messageAt) : this.#timing(request);
204
+ }
205
+
206
+ /** The calls an earlier run of this session recorded. */
207
+ restore(entries: readonly SessionEntry[]): void {
208
+ for (const entry of entries) {
209
+ if (entry.type !== "custom" || entry.customType !== REQUEST_CHANGES) continue;
210
+ const record = entry.data as Partial<RequestRecord> | undefined;
211
+ if (record?.messageAt === undefined || record.at === undefined) continue;
212
+ this.#restored.set(record.messageAt, {
213
+ at: record.at,
214
+ streaming: false,
215
+ waitMs: record.firstTokenMs,
216
+ waitingMs: record.firstTokenMs ?? 0,
217
+ serverMs: record.serverMs,
218
+ thoughtMs: record.thoughtMs,
219
+ totalMs: record.totalMs ?? 0,
220
+ writingMs: record.generationMs ?? 0,
221
+ stallMs: record.stallMs ?? 0,
222
+ output: record.output ?? 0,
223
+ estimated: false,
224
+ });
225
+ }
226
+ }
227
+
228
+ request(): RequestView | undefined {
229
+ const request = this.#request;
230
+ return request === undefined ? undefined : this.#view(request);
231
+ }
232
+
233
+ last(): RequestView | undefined {
234
+ return this.#last;
235
+ }
236
+
237
+ run(): RunView | undefined {
238
+ const run = this.#run;
239
+ if (run === undefined) return undefined;
240
+ const running = run.endedAt === undefined;
241
+ return {
242
+ running,
243
+ elapsedMs: (run.endedAt ?? this.#now()) - run.startedAt,
244
+ requests: run.requests,
245
+ cost: run.cost,
246
+ };
247
+ }
248
+
249
+ reset(): void {
250
+ this.#run = undefined;
251
+ this.#request = undefined;
252
+ this.#last = undefined;
253
+ this.#byMessage.clear();
254
+ this.#restored.clear();
255
+ this.#sentAt = undefined;
256
+ this.#headersAt = undefined;
257
+ }
258
+
259
+ #begin(): void {
260
+ this.#request = {
261
+ startedAt: this.#now(),
262
+ sentAt: this.#sentAt,
263
+ headersAt: this.#headersAt,
264
+ firstTokenAt: undefined,
265
+ firstWritingAt: undefined,
266
+ endedAt: undefined,
267
+ usage: emptyTotals(),
268
+ characters: 0,
269
+ lastDeltaAt: undefined,
270
+ stallMs: 0,
271
+ };
272
+ }
273
+
274
+ /** Pi sends before it opens the message, so the marks may arrive first. */
275
+ #attach(): void {
276
+ const request = this.#request;
277
+ if (request === undefined || request.endedAt !== undefined) return;
278
+ request.sentAt = this.#sentAt;
279
+ request.headersAt = this.#headersAt;
280
+ }
281
+
282
+ #timing(request: Request): Timing {
283
+ const view = this.#view(request);
284
+ const end = request.endedAt ?? this.#now();
285
+ const from = request.sentAt ?? request.startedAt;
286
+ // While it's paused, the pause so far counts too.
287
+ const pause = request.endedAt === undefined ? end - (request.lastDeltaAt ?? end) : 0;
288
+ return {
289
+ at: from,
290
+ streaming: view.streaming,
291
+ waitMs: view.waitMs,
292
+ waitingMs: view.waitingMs,
293
+ serverMs: view.serverMs,
294
+ thoughtMs: view.thoughtMs,
295
+ totalMs: end - from,
296
+ writingMs: request.firstTokenAt === undefined ? 0 : end - request.firstTokenAt,
297
+ stallMs: request.stallMs + (pause >= STALL_MS ? pause : 0),
298
+ output: view.usage.output,
299
+ estimated: view.estimated,
300
+ };
301
+ }
302
+
303
+ #view(request: Request): RequestView {
304
+ const now = this.#now();
305
+ const streaming = request.endedAt === undefined;
306
+ const from = request.sentAt ?? request.startedAt;
307
+ const waitMs = span(from, request.firstTokenAt);
308
+ const estimated = streaming && request.usage.output === 0;
309
+ const output = estimated
310
+ ? Math.round(request.characters / this.#charsPerToken())
311
+ : request.usage.output;
312
+ const writingMs =
313
+ request.firstTokenAt === undefined ? 0 : (request.endedAt ?? now) - request.firstTokenAt;
314
+ const thoughtMs = span(request.firstTokenAt, request.firstWritingAt);
315
+
316
+ return {
317
+ streaming,
318
+ waiting: streaming && request.firstTokenAt === undefined,
319
+ elapsedMs: (request.endedAt ?? now) - request.startedAt,
320
+ waitMs,
321
+ waitingMs: (request.endedAt ?? now) - from,
322
+ serverMs: span(request.sentAt, request.headersAt),
323
+ prefillMs: span(request.headersAt ?? request.sentAt, request.firstTokenAt),
324
+ thoughtMs: thoughtMs !== undefined && thoughtMs > 0 ? thoughtMs : undefined,
325
+ decode: writingMs < MIN_SAMPLE_MS && streaming ? undefined : perSecond(output, writingMs),
326
+ prefill: waitMs === undefined ? undefined : perSecond(promptOf(request.usage), waitMs),
327
+ usage: { ...request.usage, output },
328
+ estimated,
329
+ };
330
+ }
331
+
332
+ /** This session's ratio, or the measured default. */
333
+ #charsPerToken(): number {
334
+ const { characters, output } = this.#sampled;
335
+ return output > 0 ? characters / output : DEFAULT_CHARS_PER_TOKEN;
336
+ }
337
+ }