@huseynsnmz/pi-suite 0.0.0-stage → 0.1.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 (219) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -2
  4. package/SECURITY.md +5 -0
  5. package/package.json +113 -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 +83 -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 +398 -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 +2221 -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/frame/CHANGELOG.md +113 -0
  114. package/packages/frame/CONTRIBUTING.md +7 -0
  115. package/packages/frame/LICENSE +22 -0
  116. package/packages/frame/README.md +105 -0
  117. package/packages/frame/SECURITY.md +16 -0
  118. package/packages/frame/docs/how-it-works.md +53 -0
  119. package/packages/frame/docs/reference.md +78 -0
  120. package/packages/frame/package.json +75 -0
  121. package/packages/frame/src/data/git.ts +90 -0
  122. package/packages/frame/src/data/github.ts +140 -0
  123. package/packages/frame/src/data/gitlab.ts +63 -0
  124. package/packages/frame/src/data/mode.ts +23 -0
  125. package/packages/frame/src/data/past-prompts.ts +57 -0
  126. package/packages/frame/src/data/runner.ts +32 -0
  127. package/packages/frame/src/data/snapshot.ts +223 -0
  128. package/packages/frame/src/data/start-notes.ts +104 -0
  129. package/packages/frame/src/data/telemetry.ts +337 -0
  130. package/packages/frame/src/data/todos.ts +162 -0
  131. package/packages/frame/src/data/totals.ts +94 -0
  132. package/packages/frame/src/data/visits.ts +45 -0
  133. package/packages/frame/src/index.ts +676 -0
  134. package/packages/frame/src/render/diff.ts +104 -0
  135. package/packages/frame/src/render/fit.ts +86 -0
  136. package/packages/frame/src/render/format.ts +108 -0
  137. package/packages/frame/src/render/frame.ts +106 -0
  138. package/packages/frame/src/render/glyphs.ts +189 -0
  139. package/packages/frame/src/render/keys.ts +43 -0
  140. package/packages/frame/src/render/lead.ts +37 -0
  141. package/packages/frame/src/render/paint.ts +135 -0
  142. package/packages/frame/src/render/safe.ts +19 -0
  143. package/packages/frame/src/render/segments.ts +735 -0
  144. package/packages/frame/src/render/streaming.ts +223 -0
  145. package/packages/frame/src/render/timing.ts +106 -0
  146. package/packages/frame/src/render/tools.ts +167 -0
  147. package/packages/frame/src/settings.ts +105 -0
  148. package/packages/frame/src/ui/editor.ts +272 -0
  149. package/packages/frame/src/ui/effort.ts +36 -0
  150. package/packages/frame/src/ui/footer.ts +165 -0
  151. package/packages/frame/src/ui/header.ts +362 -0
  152. package/packages/frame/src/ui/history-search.ts +23 -0
  153. package/packages/frame/src/ui/interrupt.ts +21 -0
  154. package/packages/frame/src/ui/outputs.ts +170 -0
  155. package/packages/frame/src/ui/pi-entries.ts +276 -0
  156. package/packages/frame/src/ui/previews.ts +72 -0
  157. package/packages/frame/src/ui/queue.ts +75 -0
  158. package/packages/frame/src/ui/screen.ts +59 -0
  159. package/packages/frame/src/ui/timings.ts +48 -0
  160. package/packages/frame/src/ui/todos.ts +205 -0
  161. package/packages/frame/src/ui/tool-frame.ts +595 -0
  162. package/packages/frame/src/ui/working.ts +25 -0
  163. package/packages/frame/src/view/state.ts +97 -0
  164. package/packages/frame/src/view/transcript.ts +175 -0
  165. package/packages/frame/src/view/view.ts +162 -0
  166. package/packages/memory/CONTRIBUTING.md +7 -0
  167. package/packages/memory/LICENSE +21 -0
  168. package/packages/memory/README.md +44 -0
  169. package/packages/memory/SECURITY.md +20 -0
  170. package/packages/memory/package.json +55 -0
  171. package/packages/memory/skills/review-memory/SKILL.md +48 -0
  172. package/packages/memory/src/index.ts +163 -0
  173. package/packages/memory/src/prompt.ts +25 -0
  174. package/packages/memory/src/review.ts +150 -0
  175. package/packages/memory/src/secrets.ts +22 -0
  176. package/packages/memory/src/store.ts +173 -0
  177. package/packages/modes/CHANGELOG.md +104 -0
  178. package/packages/modes/CONTRIBUTING.md +7 -0
  179. package/packages/modes/LICENSE +21 -0
  180. package/packages/modes/README.md +174 -0
  181. package/packages/modes/SECURITY.md +17 -0
  182. package/packages/modes/package.json +55 -0
  183. package/packages/modes/skills/review-permissions/SKILL.md +54 -0
  184. package/packages/modes/src/bash.ts +218 -0
  185. package/packages/modes/src/deny-off.ts +62 -0
  186. package/packages/modes/src/git-safety.ts +72 -0
  187. package/packages/modes/src/index.ts +338 -0
  188. package/packages/modes/src/interview.ts +206 -0
  189. package/packages/modes/src/judge-gate.ts +113 -0
  190. package/packages/modes/src/modes.ts +121 -0
  191. package/packages/modes/src/review/apply.mjs +156 -0
  192. package/packages/modes/src/review/collect.mjs +208 -0
  193. package/packages/modes/src/review/tools.ts +71 -0
  194. package/packages/modes/src/sandbox.ts +37 -0
  195. package/packages/router/CHANGELOG.md +8 -0
  196. package/packages/router/LICENSE +21 -0
  197. package/packages/router/README.md +90 -0
  198. package/packages/router/package.json +51 -0
  199. package/packages/router/src/config.ts +65 -0
  200. package/packages/router/src/index.ts +172 -0
  201. package/packages/router/src/judge.ts +51 -0
  202. package/packages/router/src/route.ts +67 -0
  203. package/packages/web/CHANGELOG.md +36 -0
  204. package/packages/web/LICENSE +21 -0
  205. package/packages/web/README.md +87 -0
  206. package/packages/web/SECURITY.md +13 -0
  207. package/packages/web/package.json +61 -0
  208. package/packages/web/src/config.ts +109 -0
  209. package/packages/web/src/dnsfilter.ts +97 -0
  210. package/packages/web/src/extract.ts +65 -0
  211. package/packages/web/src/index.ts +217 -0
  212. package/packages/web/src/net.ts +109 -0
  213. package/packages/web/src/reader.ts +73 -0
  214. package/packages/web/src/sanitize.ts +50 -0
  215. package/packages/web/src/search.ts +76 -0
  216. package/packages/web/src/secrets.ts +22 -0
  217. package/packages/web/src/validate.ts +186 -0
  218. package/src/index.ts +156 -0
  219. package/src/settings.ts +78 -0
@@ -0,0 +1,676 @@
1
+ import { dirname, join } from "node:path";
2
+
3
+ import {
4
+ ACCOUNT,
5
+ type AccountState,
6
+ BOARD,
7
+ createApp,
8
+ PEERS,
9
+ PROMPT_FROM,
10
+ defineFeature,
11
+ drawSkillRefs,
12
+ type FeatureScope,
13
+ hideImagesUnderOverlays,
14
+ NAMES,
15
+ SKILL_REF,
16
+ provideViews,
17
+ type SessionSource,
18
+ skillNames,
19
+ sourceFor,
20
+ } from "@adeildo/pi-kit";
21
+ import type {
22
+ ExtensionAPI,
23
+ ExtensionContext,
24
+ ReadonlyFooterDataProvider,
25
+ } from "@earendil-works/pi-coding-agent";
26
+ import { keyText } from "@earendil-works/pi-coding-agent";
27
+ import { Key, type KeyId, type TUI } from "@earendil-works/pi-tui";
28
+
29
+ import { type GitState, readGit } from "./data/git.ts";
30
+ import { cmdAsKey, SETTINGS_SHORTCUT, watchCmdKey } from "./render/keys.ts";
31
+ import { frameColorOf } from "./data/mode.ts";
32
+ import { type GithubState, readGithub } from "./data/github.ts";
33
+ import { emptySnapshot, type LiveInputs, type Snapshot, SnapshotReader } from "./data/snapshot.ts";
34
+ import type { LastVisit } from "./data/start-notes.ts";
35
+ import { Telemetry } from "./data/telemetry.ts";
36
+ import type { DeltaKind } from "./data/telemetry.ts";
37
+ import { REQUEST_CHANGES, type RequestRecord } from "./data/totals.ts";
38
+ import { visit } from "./data/visits.ts";
39
+ import { type Glyphs, glyphsOf } from "./render/glyphs.ts";
40
+ import { PLAIN, themePaint } from "./render/paint.ts";
41
+ import { claimedStatuses, renderSegments, SEGMENTS } from "./render/segments.ts";
42
+ import {
43
+ cursor,
44
+ frame,
45
+ gaugeCells,
46
+ header,
47
+ icons,
48
+ labels,
49
+ FRAME_SETTINGS,
50
+ pathLength,
51
+ SECTIONS,
52
+ SEPARATOR,
53
+ SLOTS,
54
+ userFrame,
55
+ timing,
56
+ } from "./settings.ts";
57
+ import { pastPrompts } from "./data/past-prompts.ts";
58
+ import { applyCursor, FrameEditor } from "./ui/editor.ts";
59
+ import { registerEffort } from "./ui/effort.ts";
60
+ import { FooterComponent, StripComponent } from "./ui/footer.ts";
61
+ import { PACKAGE_VERSION, HeaderComponent, type LoadedCounts } from "./ui/header.ts";
62
+ import { EXIT_WINDOW_MS, type InterruptState, nextInterrupt } from "./ui/interrupt.ts";
63
+ import { OutputView, outputsOf } from "./ui/outputs.ts";
64
+ import { framePiEntries, loadedLists, onLoadedListsChange } from "./ui/pi-entries.ts";
65
+ import { showTimings } from "./ui/timings.ts";
66
+ import { timingLine } from "./render/timing.ts";
67
+ import { frameQueue } from "./ui/queue.ts";
68
+ import type { Screen, SlotName } from "./ui/screen.ts";
69
+ import { registerTodos } from "./ui/todos.ts";
70
+ import { registerToolFrames } from "./ui/tool-frame.ts";
71
+ import { type Activity, describe, draftedTool } from "./ui/working.ts";
72
+ import { SessionView } from "./view/view.ts";
73
+
74
+ export { SEGMENT_IDS, SEGMENTS, type SegmentId } from "./render/segments.ts";
75
+ import type { SegmentId } from "./render/segments.ts";
76
+ export { ROLE_TOKENS } from "./render/paint.ts";
77
+
78
+ const STRIP_WIDGET = "pi-frame:strip";
79
+ const TICK_MS = 500;
80
+ // Once per process, however many sessions it opens.
81
+ let probedGithub = false;
82
+
83
+ interface Live {
84
+ ctx: ExtensionContext | undefined;
85
+ footerData: ReadonlyFooterDataProvider | undefined;
86
+ tui: TUI | undefined;
87
+ repaint: (() => void) | undefined;
88
+ git: GitState | undefined;
89
+ github: GithubState | undefined;
90
+ probing: boolean;
91
+ ticker: ReturnType<typeof setInterval> | undefined;
92
+ frame: Snapshot | undefined;
93
+ activity: string | undefined;
94
+ interrupts: InterruptState;
95
+ notice: string | undefined;
96
+ noticeTimer: ReturnType<typeof setTimeout> | undefined;
97
+ }
98
+
99
+ export const frameFeature = defineFeature({
100
+ id: "frame",
101
+ description:
102
+ "The start screen, a framed editor, the footer and a working line that says what runs",
103
+ sections: SECTIONS,
104
+ settings: FRAME_SETTINGS,
105
+ setup(scope) {
106
+ const telemetry = new Telemetry();
107
+ const reader = new SnapshotReader();
108
+ const live: Live = {
109
+ ctx: undefined,
110
+ footerData: undefined,
111
+ tui: undefined,
112
+ repaint: undefined,
113
+ git: undefined,
114
+ github: undefined,
115
+ probing: false,
116
+ ticker: undefined,
117
+ frame: undefined,
118
+ activity: undefined,
119
+ interrupts: {},
120
+ notice: undefined,
121
+ noticeTimer: undefined,
122
+ };
123
+
124
+ const glyphs = glyphsOf(() => icons.get(scope));
125
+ const screen = createScreen(scope, live, reader, telemetry, glyphs);
126
+ registerToolFrames(scope);
127
+ registerEffort(scope);
128
+ registerTodos(scope, () => glyphs().set);
129
+ framePiEntries(() => {
130
+ if (live.ctx === undefined) return undefined;
131
+ return {
132
+ theme: live.ctx.ui.theme,
133
+ set: glyphs().set,
134
+ you: userFrame.get(scope),
135
+ from: (text: string) => PROMPT_FROM.current(scope, text)?.name,
136
+ };
137
+ });
138
+ showTimings((message, width) => {
139
+ if (live.ctx === undefined || !timing.get(scope)) return undefined;
140
+ const line = timingLine(telemetry.timingOf(message.timestamp), message, width);
141
+ return live.ctx.ui.theme.fg("dim", line);
142
+ });
143
+ onLoadedListsChange(() => queueMicrotask(() => live.repaint?.()));
144
+ frameQueue(() => live.ctx?.ui.theme);
145
+ const repaint = () => {
146
+ screen.bump();
147
+ live.repaint?.();
148
+ };
149
+
150
+ for (const entry of FRAME_SETTINGS) entry.listen(scope, repaint);
151
+ frame.listen(scope, () => live.ctx && installEditor(live.ctx, screen, live, scope));
152
+ const visitsFile = join(dirname(scope.settings.globalPath), "frame-visits.json");
153
+ let visited = false;
154
+ let lastVisit: LastVisit | undefined;
155
+
156
+ provideViews(
157
+ scope.events,
158
+ (address) => {
159
+ const ctx = live.ctx;
160
+ const source = sourceFor(scope.events, address);
161
+ if (ctx === undefined || ctx.mode !== "tui" || source === undefined) return false;
162
+ openView(ctx, screen, source).catch((error: unknown) => {
163
+ scope.warn(`pi-frame: could not show ${address.session}: ${String(error)}`);
164
+ });
165
+ return true;
166
+ },
167
+ () => live.ctx?.mode === "tui",
168
+ );
169
+
170
+ scope.registerShortcut(Key.alt("o"), {
171
+ description: "A tool call's whole output, to scroll; the arrows move between calls",
172
+ handler: (ctx) => openOutputs(ctx),
173
+ });
174
+
175
+ let stopCmdKeys: (() => void) | undefined;
176
+ watchCmdKey(() => live.tui?.terminal.kittyProtocolActive === true);
177
+
178
+ const installFooter = (ctx: ExtensionContext) =>
179
+ ctx.ui.setFooter(
180
+ (tui, _theme, data) =>
181
+ new FooterComponent(tui, data, screen, {
182
+ attach: (footerData, paint) => {
183
+ live.footerData = footerData;
184
+ // Anything that asks for a redraw bumps the revision that kept drawings compare.
185
+ live.repaint = () => {
186
+ screen.bump();
187
+ paint();
188
+ };
189
+ },
190
+ detach: () => {
191
+ live.footerData = undefined;
192
+ live.repaint = undefined;
193
+ },
194
+ }),
195
+ );
196
+
197
+ const showHeader = (ctx: ExtensionContext) =>
198
+ installHeader(
199
+ ctx,
200
+ screen,
201
+ scope,
202
+ () => lastVisit,
203
+ () => live.github,
204
+ );
205
+
206
+ // A board blanks these while it's up, and Pi can only give back its own.
207
+ scope.events.on("agent-board:closed", () => {
208
+ const ctx = live.ctx;
209
+ if (ctx?.mode !== "tui") return;
210
+ installFooter(ctx);
211
+ showHeader(ctx);
212
+ });
213
+
214
+ scope.onSessionStart((ctx) => {
215
+ live.ctx = ctx;
216
+ telemetry.reset();
217
+ telemetry.restore(ctx.sessionManager.getEntries());
218
+ void probeGit(live);
219
+ if (ctx.mode !== "tui") return;
220
+
221
+ stopCmdKeys?.();
222
+ stopCmdKeys = ctx.ui.onTerminalInput((data) => {
223
+ const key = cmdAsKey(data);
224
+ return key === undefined ? undefined : { data: key };
225
+ });
226
+
227
+ installFooter(ctx);
228
+ ctx.ui.setWidget(STRIP_WIDGET, () => new StripComponent(screen), {
229
+ placement: "aboveEditor",
230
+ });
231
+ installEditor(ctx, screen, live, scope);
232
+ // A new session in the same pi is not a return to the folder.
233
+ if (!visited) {
234
+ visited = true;
235
+ lastVisit = visit(visitsFile, ctx.cwd, PACKAGE_VERSION, Date.now());
236
+ }
237
+ // Off the critical path: the card shows these lines when the answers arrive.
238
+ if (!probedGithub) {
239
+ probedGithub = true;
240
+ void probeGithub(live, lastVisit?.at);
241
+ }
242
+ showHeader(ctx);
243
+ });
244
+
245
+ scope.onShutdown(() => {
246
+ stopCmdKeys?.();
247
+ stopCmdKeys = undefined;
248
+ stopTicking(live);
249
+ const ctx = live.ctx;
250
+ if (ctx?.mode === "tui") {
251
+ ctx.ui.setFooter(undefined);
252
+ ctx.ui.setWidget(STRIP_WIDGET, undefined);
253
+ ctx.ui.setHeader(undefined);
254
+ ctx.ui.setEditorComponent(undefined);
255
+ ctx.ui.setWorkingMessage();
256
+ }
257
+ if (live.tui !== undefined) applyCursor(live.tui, "block");
258
+ live.ctx = undefined;
259
+ live.tui = undefined;
260
+ });
261
+
262
+ // Only for the agent's calls, not pi's cache warming.
263
+ scope.on("before_provider_request", () => telemetry.providerRequestSent());
264
+ scope.on("after_provider_response", () => telemetry.providerResponseArrived());
265
+
266
+ scope.on("agent_start", () => {
267
+ telemetry.runStarted();
268
+ setActivity(live, { kind: "waiting" });
269
+ startTicking(live);
270
+ });
271
+
272
+ scope.on("turn_start", () => {
273
+ telemetry.requestStarted();
274
+ setActivity(live, { kind: "waiting" });
275
+ });
276
+
277
+ scope.on("message_start", (event) => {
278
+ if (event.message.role === "assistant") telemetry.answerStarted(event.message.timestamp);
279
+ });
280
+
281
+ scope.on("message_update", (event) => {
282
+ const message = event.message;
283
+ if (message.role !== "assistant") return;
284
+ const update = event.assistantMessageEvent;
285
+ const delta = "delta" in update ? update.delta : "";
286
+ const kind: DeltaKind = update.type.startsWith("thinking") ? "thinking" : "writing";
287
+ telemetry.answerGrew(delta, message.usage, kind);
288
+
289
+ if (update.type === "thinking_start" || update.type === "thinking_delta") {
290
+ setActivity(live, { kind: "thinking" });
291
+ } else if (update.type === "text_delta" || update.type === "text_start") {
292
+ setActivity(live, { kind: "writing" });
293
+ } else if (update.type === "toolcall_start" || update.type === "toolcall_delta") {
294
+ const tool = draftedTool(message.content);
295
+ if (tool !== undefined) setActivity(live, { kind: "drafting", tool });
296
+ }
297
+ });
298
+
299
+ scope.on("message_end", (event) => {
300
+ if (event.message.role !== "assistant") return;
301
+ const record = telemetry.answerEnded(event.message.usage);
302
+ // Pi records usage without a duration, so the session keeps it for the speeds after a resume.
303
+ if (record !== undefined) scope.appendEntry<RequestRecord>(REQUEST_CHANGES, record);
304
+ repaint();
305
+ });
306
+
307
+ scope.on("tool_execution_start", (event) => {
308
+ setActivity(live, { kind: "running", tool: event.toolName });
309
+ });
310
+
311
+ scope.on("tool_execution_end", () => void probeGit(live));
312
+
313
+ scope.on("agent_end", () => {
314
+ telemetry.runEnded();
315
+ stopTicking(live);
316
+ live.activity = undefined;
317
+ live.ctx?.ui.setWorkingMessage();
318
+ repaint();
319
+ });
320
+
321
+ scope.on("agent_settled", () => void probeGit(live));
322
+ scope.on("model_select", repaint);
323
+ scope.on("thinking_level_select", repaint);
324
+ scope.on("session_compact", repaint);
325
+ scope.on("session_tree", repaint);
326
+
327
+ scope.screen.action({
328
+ id: "explain",
329
+ section: "Appearance",
330
+ label: "What is on screen",
331
+ description: "Every piece around the editor right now, and what it means.",
332
+ run: () => explain(screen),
333
+ });
334
+ },
335
+ });
336
+
337
+ const SLOT_TITLES: Record<SlotName, string> = {
338
+ strip: "Above the editor, this answer",
339
+ stripRight: "Above the editor, on the right",
340
+ topLeft: "Frame, top left",
341
+ topRight: "Frame, top right",
342
+ bottomLeft: "Frame, bottom left",
343
+ bottomRight: "Frame, bottom right",
344
+ below: "Below the editor, the session",
345
+ belowRight: "Below the editor, on the right",
346
+ };
347
+
348
+ function explain(screen: Screen): string {
349
+ const snapshot = screen.snapshot();
350
+ const paint = screen.paint();
351
+ const lines: string[] = [paint.bold("What is on screen")];
352
+ const groups: [string, readonly SegmentId[]][] = [
353
+ ...(Object.keys(SLOT_TITLES) as SlotName[]).map((name): [string, readonly SegmentId[]] => [
354
+ SLOT_TITLES[name],
355
+ screen.slot(name),
356
+ ]),
357
+ // Not on screen: kept for when an answer feels slow.
358
+ ["The last call to the model", ["last", "wait", "speed", "server", "request"]],
359
+ ];
360
+ for (const [title, ids] of groups) {
361
+ if (ids.length === 0) continue;
362
+ lines.push("", paint.role("brand", title));
363
+ for (const id of ids) {
364
+ const [piece] = renderSegments([id], {
365
+ snapshot,
366
+ glyphs: screen.glyphs(),
367
+ paint,
368
+ options: screen.options(),
369
+ });
370
+ const meaning = id.startsWith("status:")
371
+ ? `what ${id.slice("status:".length)} reports`
372
+ : (SEGMENTS[id as keyof typeof SEGMENTS]?.describe ?? "");
373
+ const shown = piece === undefined ? paint.dim("(nothing yet)") : piece.text;
374
+ lines.push(` ${shown}`, ` ${paint.dim(`${id}: ${meaning}`)}`);
375
+ }
376
+ }
377
+ lines.push("", paint.dim("In is what was sent to the model, out is what came back."));
378
+ return lines.join("\n");
379
+ }
380
+
381
+ function createScreen(
382
+ scope: FeatureScope,
383
+ live: Live,
384
+ reader: SnapshotReader,
385
+ telemetry: Telemetry,
386
+ glyphs: () => Glyphs,
387
+ ): Screen {
388
+ let revision = 0;
389
+ return {
390
+ snapshot() {
391
+ const ctx = live.ctx;
392
+ if (ctx === undefined) return emptySnapshot();
393
+ if (live.frame !== undefined) return live.frame;
394
+ const snapshot = reader.read(ctx, {
395
+ branch: live.footerData?.getGitBranch() ?? null,
396
+ git: live.git,
397
+ account: accountOf(scope, ctx.model?.provider),
398
+ request: telemetry.request(),
399
+ last: telemetry.last(),
400
+ run: telemetry.run(),
401
+ statuses: live.footerData?.getExtensionStatuses() ?? new Map(),
402
+ sandboxInstalled: sandboxInstalled(scope),
403
+ ...boardAndPeersOf(scope),
404
+ });
405
+ live.frame = snapshot;
406
+ queueMicrotask(() => {
407
+ live.frame = undefined;
408
+ });
409
+ return snapshot;
410
+ },
411
+ glyphs,
412
+ revision: () => revision,
413
+ bump: () => {
414
+ revision++;
415
+ },
416
+ paint: (border) => (live.ctx === undefined ? PLAIN : themePaint(live.ctx.ui.theme, border)),
417
+ separator: () => SEPARATOR,
418
+ options: () => ({
419
+ pathLength,
420
+ gaugeCells,
421
+ labels,
422
+ claimed: claimedStatuses(Object.values(SLOTS)),
423
+ }),
424
+ statusesKey: () => [...(live.footerData?.getExtensionStatuses().values() ?? [])].join("\u0000"),
425
+ notice: () => {
426
+ const theme = live.ctx?.ui.theme;
427
+ return live.notice === undefined || theme === undefined
428
+ ? undefined
429
+ : theme.fg("warning", live.notice);
430
+ },
431
+ interrupt: (clearEditor) => pressCtrlC(live, clearEditor),
432
+ slot: (name) => SLOTS[name],
433
+ promptColor: (kind) => {
434
+ const theme = live.ctx?.ui.theme;
435
+ if (theme === undefined) return (text) => text;
436
+ return (text) => theme.fg(kind === "bash" ? "bashMode" : "accent", text);
437
+ },
438
+ pastPrompts: () => {
439
+ try {
440
+ return pastPrompts(live.ctx?.sessionManager.getSessionDir() ?? "");
441
+ } catch {
442
+ return [];
443
+ }
444
+ },
445
+ frameStyle: () => frame.get(scope),
446
+ frameColor: () => {
447
+ const theme = live.ctx?.ui.theme;
448
+ const color = frameColorOf(live.footerData?.getExtensionStatuses() ?? new Map());
449
+ return theme === undefined || color === undefined
450
+ ? undefined
451
+ : (text) => theme.fg(color, text);
452
+ },
453
+ cursor: () => cursor,
454
+ decorate: (line) => {
455
+ const theme = live.ctx?.ui.theme;
456
+ if (theme === undefined) return line;
457
+ return drawSkillRefs(line, skillNames(scope), (name) =>
458
+ theme.fg("customMessageLabel", `${SKILL_REF}${name}`),
459
+ );
460
+ },
461
+ skills: () => skillNames(scope).length > 0,
462
+ };
463
+ }
464
+
465
+ function accountOf(scope: FeatureScope, provider: string | undefined): AccountState | undefined {
466
+ return provider === undefined ? undefined : ACCOUNT.current(scope, provider);
467
+ }
468
+
469
+ function boardAndPeersOf(
470
+ scope: FeatureScope,
471
+ ): Pick<LiveInputs, "board" | "me" | "peers" | "peersRooms" | "peersUnread" | "peersWriting"> {
472
+ const peers = PEERS.current(scope);
473
+ const handle = NAMES.current(scope)?.handle;
474
+ const color = peers?.me?.handle === handle ? peers?.me?.color : undefined;
475
+ return {
476
+ board: BOARD.current(scope),
477
+ me: handle === undefined ? undefined : { handle, ...(color === undefined ? {} : { color }) },
478
+ peers: peers?.peers,
479
+ peersRooms: peers?.rooms,
480
+ peersUnread: peers?.unread,
481
+ peersWriting: peers?.writing,
482
+ };
483
+ }
484
+
485
+ function pressCtrlC(live: Live, clearEditor: () => void): void {
486
+ const ctx = live.ctx;
487
+ if (ctx === undefined) return;
488
+ const now = Date.now();
489
+ const step = nextInterrupt(live.interrupts, ctx.isIdle(), now);
490
+ live.interrupts = step.state;
491
+ if (step.action === "exit") {
492
+ ctx.shutdown();
493
+ return;
494
+ }
495
+ if (step.action === "abort") ctx.abort();
496
+ else clearEditor();
497
+ live.notice = step.action === "abort" ? "Aborted" : `Press ${keyText("app.clear")} again to exit`;
498
+ if (live.noticeTimer !== undefined) clearTimeout(live.noticeTimer);
499
+ live.noticeTimer = setTimeout(() => {
500
+ live.notice = undefined;
501
+ live.interrupts = {};
502
+ live.repaint?.();
503
+ }, EXIT_WINDOW_MS);
504
+ live.noticeTimer.unref?.();
505
+ live.repaint?.();
506
+ }
507
+
508
+ function installEditor(
509
+ ctx: ExtensionContext,
510
+ screen: Screen,
511
+ live: Live,
512
+ scope: FeatureScope,
513
+ ): void {
514
+ if (ctx.mode !== "tui") return;
515
+ if (frame.get(scope) === "off") {
516
+ ctx.ui.setEditorComponent(undefined);
517
+ return;
518
+ }
519
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => {
520
+ live.tui = tui;
521
+ applyCursor(tui, cursor);
522
+ return new FrameEditor(tui, theme, keybindings, screen);
523
+ });
524
+ }
525
+
526
+ function installHeader(
527
+ ctx: ExtensionContext,
528
+ screen: Screen,
529
+ scope: FeatureScope,
530
+ lastVisit: () => LastVisit | undefined,
531
+ github: () => GithubState | undefined,
532
+ ): void {
533
+ if (ctx.mode !== "tui") return;
534
+ ctx.ui.setHeader(
535
+ (tui) =>
536
+ new HeaderComponent(
537
+ screen,
538
+ {
539
+ style: () => header,
540
+ counts: () => loadedCounts(scope),
541
+ lastVisit,
542
+ github,
543
+ loaded: loadedLists,
544
+ },
545
+ tui,
546
+ ),
547
+ );
548
+ }
549
+
550
+ function sandboxInstalled(pi: ExtensionAPI): boolean {
551
+ return pi.getCommands().some((command) => command.name === "sandbox-enable");
552
+ }
553
+
554
+ function loadedCounts(pi: ExtensionAPI): LoadedCounts {
555
+ const commands = pi.getCommands();
556
+ const extensions = new Set<string>();
557
+ for (const command of commands) {
558
+ if (command.source === "extension") extensions.add(command.sourceInfo.path);
559
+ }
560
+ for (const tool of pi.getAllTools()) {
561
+ const source = tool.sourceInfo.source;
562
+ if (source !== "builtin" && source !== "sdk") extensions.add(tool.sourceInfo.path);
563
+ }
564
+ return {
565
+ tools: pi.getActiveTools().length,
566
+ skills: commands.filter((command) => command.source === "skill").length,
567
+ prompts: commands.filter((command) => command.source === "prompt").length,
568
+ extensions: extensions.size,
569
+ };
570
+ }
571
+
572
+ function setActivity(live: Live, activity: Activity): void {
573
+ const message = describe(activity);
574
+ if (message === live.activity) return;
575
+ live.activity = message;
576
+ live.ctx?.ui.setWorkingMessage(message);
577
+ }
578
+
579
+ async function probeGithub(live: Live, since: number | undefined): Promise<void> {
580
+ const ctx = live.ctx;
581
+ if (ctx === undefined) return;
582
+ const github = await readGithub(ctx.cwd, since);
583
+ if (live.ctx === undefined) return;
584
+ live.github = github;
585
+ // The card keeps its drawing for a second; this makes the new lines appear.
586
+ live.repaint?.();
587
+ }
588
+
589
+ async function probeGit(live: Live): Promise<void> {
590
+ const ctx = live.ctx;
591
+ if (ctx === undefined || live.probing) return;
592
+ live.probing = true;
593
+ try {
594
+ live.git = await readGit(ctx.cwd);
595
+ live.repaint?.();
596
+ } finally {
597
+ live.probing = false;
598
+ }
599
+ }
600
+
601
+ function startTicking(live: Live): void {
602
+ stopTicking(live);
603
+ live.ticker = setInterval(() => live.repaint?.(), TICK_MS);
604
+ live.ticker.unref?.();
605
+ }
606
+
607
+ function stopTicking(live: Live): void {
608
+ if (live.ticker === undefined) return;
609
+ clearInterval(live.ticker);
610
+ live.ticker = undefined;
611
+ }
612
+
613
+ /** The kit's settings screen asks for Alt+S, which pi-sandbox also needs and can't give up. */
614
+ function withSettingsKey(pi: ExtensionAPI): ExtensionAPI {
615
+ return new Proxy(pi, {
616
+ get(target, name) {
617
+ if (name === "registerShortcut") {
618
+ return (key: KeyId, options: Parameters<ExtensionAPI["registerShortcut"]>[1]) =>
619
+ target.registerShortcut(key === Key.alt("s") ? SETTINGS_SHORTCUT : key, options);
620
+ }
621
+ if (name === "registerCommand") {
622
+ return (command: string, options: Parameters<ExtensionAPI["registerCommand"]>[1]) =>
623
+ target.registerCommand(command, {
624
+ ...options,
625
+ description: options.description?.replace("Alt+S", "Ctrl+S"),
626
+ });
627
+ }
628
+ const value: unknown = Reflect.get(target, name);
629
+ return typeof value === "function" ? value.bind(target) : value;
630
+ },
631
+ });
632
+ }
633
+
634
+ export default function piLook(pi: ExtensionAPI): void {
635
+ createApp(withSettingsKey(pi), { name: "pi-frame" }).use(frameFeature).build();
636
+ }
637
+
638
+ async function openView(
639
+ ctx: ExtensionContext,
640
+ screen: Screen,
641
+ source: SessionSource,
642
+ ): Promise<void> {
643
+ let view: SessionView | undefined;
644
+ await ctx.ui.custom<void>(
645
+ (tui, theme, _keybindings, done) => {
646
+ hideImagesUnderOverlays(tui);
647
+ view = new SessionView({ tui, theme, screen, source, close: () => done() });
648
+ return view;
649
+ },
650
+ {
651
+ overlay: true,
652
+ overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
653
+ },
654
+ );
655
+ view?.dispose();
656
+ }
657
+
658
+ async function openOutputs(ctx: ExtensionContext): Promise<void> {
659
+ if (ctx.mode !== "tui") return;
660
+ const outputs = outputsOf(ctx.sessionManager.getBranch());
661
+ await ctx.ui.custom<void>(
662
+ (tui, theme, _keybindings, done) => {
663
+ hideImagesUnderOverlays(tui);
664
+ return new OutputView({
665
+ theme,
666
+ outputs,
667
+ rows: () => Math.floor(tui.terminal.rows * 0.85),
668
+ close: () => done(),
669
+ });
670
+ },
671
+ {
672
+ overlay: true,
673
+ overlayOptions: { anchor: "center", width: "90%", minWidth: 60, maxHeight: "85%" },
674
+ },
675
+ );
676
+ }