@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 { readFileSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ // While the model writes an edit or a write, these show the lines arriving, with a cursor.
5
+ import { GLYPH, isObject, tildify } from "../kit/index.ts";
6
+ import { getLanguageFromPath, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
7
+
8
+ import { tabs } from "./format.ts";
9
+ import { plainText } from "./safe.ts";
10
+
11
+ export interface Stream {
12
+ lines: string[];
13
+ progress: string[];
14
+ }
15
+
16
+ interface StreamContext {
17
+ cwd: string;
18
+ expanded: boolean;
19
+ /** The cursor shows and the last key may be cut short. */
20
+ writing: boolean;
21
+ /** Keeps what was read from disk across renders of one call. */
22
+ state: { streamTarget?: Target };
23
+ }
24
+
25
+ type Streamer = (args: unknown, theme: Theme, context: StreamContext) => Stream | undefined;
26
+
27
+ const WRITE_TAIL = 8;
28
+ const EDIT_TAIL = 10;
29
+ const CURSOR = GLYPH.cursor;
30
+ /** Larger files are not read to count their lines. */
31
+ const COUNT_LIMIT = 4 * 1024 * 1024;
32
+
33
+ export const STREAMERS: Readonly<Record<string, Streamer>> = {
34
+ write: writeStream,
35
+ edit: editStream,
36
+ };
37
+
38
+ interface Target {
39
+ path: string;
40
+ lines?: number;
41
+ exists: boolean;
42
+ }
43
+
44
+ function writeStream(args: unknown, theme: Theme, context: StreamContext): Stream | undefined {
45
+ // The model may write the content before the path.
46
+ const content = field(args, "content");
47
+ if (content === undefined) return undefined;
48
+ const path = pathOf(args);
49
+
50
+ // A path still arriving may name another file on the way.
51
+ const target =
52
+ path !== undefined && pathSettled(args, context) ? targetOf(path, context) : undefined;
53
+ const note = target === undefined ? "" : ` ${theme.fg("muted", replaces(target))}`;
54
+ const lines = content.split("\n");
55
+ const shown = context.expanded ? lines : lines.slice(-WRITE_TAIL);
56
+ const first = lines.length - shown.length;
57
+ const gutter = String(lines.length).length;
58
+ const lang = path === undefined ? undefined : getLanguageFromPath(path);
59
+ const painted = lang
60
+ ? highlightCode(shown.map(printable).join("\n"), lang)
61
+ : shown.map((line) => theme.fg("toolOutput", printable(line)));
62
+ const body = shown.map(
63
+ (_, index) =>
64
+ `${theme.fg("dim", String(first + index + 1).padStart(gutter))} ${painted[index] ?? ""}`,
65
+ );
66
+ return {
67
+ lines: [
68
+ pathLine("write", path, theme) + note,
69
+ ...above(first, gutter, theme),
70
+ ...withCursor(body, theme, context.writing),
71
+ ],
72
+ progress: [`${lines.length} line${lines.length === 1 ? "" : "s"}`, size(content)],
73
+ };
74
+ }
75
+
76
+ function editStream(args: unknown, theme: Theme, context: StreamContext): Stream | undefined {
77
+ const path = pathOf(args);
78
+ const edits = editsOf(args);
79
+ if (edits.length === 0) return undefined;
80
+
81
+ let removed = 0;
82
+ let added = 0;
83
+ const diff: string[] = [];
84
+ for (const [index, edit] of edits.entries()) {
85
+ if (index > 0) diff.push(theme.fg("dim", GLYPH.ellipsisAcross));
86
+ // Only the last edit can still be arriving.
87
+ const lines = editLines(edit, !context.writing || index < edits.length - 1);
88
+ for (const line of lines.before) diff.push(theme.fg("toolDiffContext", ` ${printable(line)}`));
89
+ for (const line of lines.out) diff.push(theme.fg("toolDiffRemoved", `-${printable(line)}`));
90
+ for (const line of lines.in) diff.push(theme.fg("toolDiffAdded", `+${printable(line)}`));
91
+ for (const line of lines.after) diff.push(theme.fg("toolDiffContext", ` ${printable(line)}`));
92
+ removed += lines.out.length;
93
+ added += lines.in.length;
94
+ }
95
+ const shown = context.expanded ? diff : diff.slice(-EDIT_TAIL);
96
+ const hidden = diff.length - shown.length;
97
+ return {
98
+ lines: [
99
+ pathLine("edit", path, theme),
100
+ ...above(hidden, 0, theme),
101
+ ...withCursor(shown, theme, context.writing),
102
+ ],
103
+ progress: [...(edits.length > 1 ? [`edit ${edits.length}`] : []), `-${removed} +${added}`],
104
+ };
105
+ }
106
+
107
+ /** Lines both texts start and end with are context; a line not repeated yet counts as taken out. */
108
+ export function editLines(
109
+ edit: PartialEdit,
110
+ complete: boolean,
111
+ ): { before: string[]; out: string[]; in: string[]; after: string[] } {
112
+ const old = edit.oldText?.split("\n") ?? [];
113
+ const next = edit.newText?.split("\n") ?? [];
114
+ // The last line of a text still arriving may be cut short.
115
+ const settled = complete ? next.length : next.length - 1;
116
+ let start = 0;
117
+ while (start < Math.min(old.length, settled) && old[start] === next[start]) start++;
118
+ let end = 0;
119
+ if (complete)
120
+ while (
121
+ end < Math.min(old.length, next.length) - start &&
122
+ old[old.length - 1 - end] === next[next.length - 1 - end]
123
+ )
124
+ end++;
125
+ return {
126
+ before: old.slice(0, start),
127
+ out: old.slice(start, old.length - end),
128
+ in: next.slice(start, next.length - end),
129
+ after: old.slice(old.length - end),
130
+ };
131
+ }
132
+
133
+ /** Pi's own renderer draws the same first line, so nothing moves when it takes over. */
134
+ export function pathLine(tool: string, path: string | undefined, theme: Theme): string {
135
+ const title = theme.fg("toolTitle", theme.bold(tool));
136
+ if (path === undefined) return `${title} ${theme.fg("toolOutput", "...")}`;
137
+ return `${title} ${theme.fg("accent", printable(tildify(path)))}`;
138
+ }
139
+
140
+ /** A carriage return would cross the border, and escape codes would reach the terminal. */
141
+ export function printable(line: string): string {
142
+ return tabs(plainText(line).replace(/\n/g, ""));
143
+ }
144
+
145
+ /** A key the model moved past is complete, and so is every key once the call is written. */
146
+ function pathSettled(args: unknown, context: StreamContext): boolean {
147
+ if (!context.writing || !isObject(args)) return true;
148
+ const last = Object.keys(args).at(-1);
149
+ return last !== "path" && last !== "file_path";
150
+ }
151
+
152
+ function above(hidden: number, gutter: number, theme: Theme): string[] {
153
+ if (hidden <= 0) return [];
154
+ const lines = `${hidden} line${hidden === 1 ? "" : "s"} above`;
155
+ return [
156
+ `${" ".repeat(Math.max(0, gutter - 1))}${theme.fg("dim", GLYPH.ellipsisUp)} ${theme.fg("muted", lines)}`,
157
+ ];
158
+ }
159
+
160
+ function withCursor(lines: readonly string[], theme: Theme, writing: boolean): string[] {
161
+ if (!writing || lines.length === 0) return [...lines];
162
+ return [...lines.slice(0, -1), `${lines.at(-1)}${theme.fg("accent", CURSOR)}`];
163
+ }
164
+
165
+ function replaces(target: Target): string {
166
+ if (!target.exists) return "new file";
167
+ if (target.lines === undefined) return "replaces a file";
168
+ return `replaces ${target.lines} line${target.lines === 1 ? "" : "s"}`;
169
+ }
170
+
171
+ /** Once per call, when the path is complete. */
172
+ function targetOf(path: string, context: StreamContext): Target {
173
+ const known = context.state.streamTarget;
174
+ if (known?.path === path) return known;
175
+ const full = resolve(context.cwd, path);
176
+ let target: Target = { path, exists: false };
177
+ try {
178
+ const info = statSync(full);
179
+ target = { path, exists: true };
180
+ if (info.isFile() && info.size <= COUNT_LIMIT) {
181
+ const text = readFileSync(full, "utf8");
182
+ target.lines = text === "" ? 0 : text.replace(/\n$/, "").split("\n").length;
183
+ }
184
+ } catch (error) {
185
+ // Missing is a new file; unreadable is still a file.
186
+ target = { path, exists: (error as NodeJS.ErrnoException).code !== "ENOENT" };
187
+ }
188
+ context.state.streamTarget = target;
189
+ return target;
190
+ }
191
+
192
+ function size(text: string): string {
193
+ const bytes = Buffer.byteLength(text);
194
+ if (bytes < 1024) return `${bytes} B`;
195
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
196
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
197
+ }
198
+
199
+ function pathOf(args: unknown): string | undefined {
200
+ const path = field(args, "path") ?? field(args, "file_path");
201
+ return path === "" ? undefined : path;
202
+ }
203
+
204
+ function field(args: unknown, key: string): string | undefined {
205
+ const value = isObject(args) ? args[key] : undefined;
206
+ return typeof value === "string" ? value : undefined;
207
+ }
208
+
209
+ export interface PartialEdit {
210
+ oldText?: string;
211
+ newText?: string;
212
+ }
213
+
214
+ function editsOf(args: unknown): PartialEdit[] {
215
+ if (!isObject(args)) return [];
216
+ const { edits } = args;
217
+ const list = Array.isArray(edits) ? edits : [args];
218
+ return list.flatMap((edit) => {
219
+ const oldText = field(edit, "oldText");
220
+ const newText = field(edit, "newText");
221
+ return oldText === undefined && newText === undefined ? [] : [{ oldText, newText }];
222
+ });
223
+ }
@@ -0,0 +1,106 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+
3
+ import type { Timing } from "../data/telemetry.ts";
4
+ import { latency, rate } from "./format.ts";
5
+
6
+ /** From here on, a pause while writing is worth pointing out. */
7
+ export const STALL_MS = 2_000;
8
+
9
+ export function clockTime(at: number): string {
10
+ const date = new Date(at);
11
+ const two = (value: number) => String(value).padStart(2, "0");
12
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
13
+ return `${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}.${ms}`;
14
+ }
15
+
16
+ /** To the millisecond, as `812ms`, `4.218s` or `1m 04.218s`. */
17
+ export function exactly(ms: number): string {
18
+ const whole = Math.round(ms);
19
+ if (whole < 1000) return `${whole}ms`;
20
+ if (whole < 60_000) return `${(whole / 1000).toFixed(3)}s`;
21
+ const seconds = ((whole % 60_000) / 1000).toFixed(3).padStart(6, "0");
22
+ return `${Math.floor(whole / 60_000)}m ${seconds}s`;
23
+ }
24
+
25
+ export interface AnsweredMessage {
26
+ timestamp: number;
27
+ stopReason?: string;
28
+ model?: string;
29
+ }
30
+
31
+ /** Each with its rank: the highest goes first when the line runs out of room. */
32
+ type Part = [text: string, drop: number];
33
+
34
+ function speedOf(timing: Timing): string | undefined {
35
+ const writing = timing.writingMs - timing.stallMs;
36
+ if (writing <= 0 || timing.output === 0) return undefined;
37
+ const speed = rate((timing.output / writing) * 1000);
38
+ return `${timing.estimated ? "~" : ""}${speed} tok/s`;
39
+ }
40
+
41
+ function partsOf(timing: Timing | undefined, message: AnsweredMessage): Part[] {
42
+ // A session from before this was recorded, or another session's: only the clock is known.
43
+ if (timing === undefined) return [[clockTime(message.timestamp), 0]];
44
+ const clock: Part = [clockTime(timing.at), 0];
45
+ const ttft: Part[] = timing.waitMs === undefined ? [] : [[`ttft ${latency(timing.waitMs)}`, 2]];
46
+ const stall: Part[] = timing.stallMs >= STALL_MS ? [[`stall ${latency(timing.stallMs)}`, 3]] : [];
47
+ const speed = speedOf(timing);
48
+ const rated: Part[] = speed === undefined ? [] : [[speed, 4]];
49
+
50
+ if (timing.streaming) {
51
+ if (timing.waitMs === undefined) return [clock, [`waiting ${latency(timing.waitingMs)}`, 0]];
52
+ return [clock, ...ttft, [`writing ${latency(timing.writingMs)}`, 0], ...stall, ...rated];
53
+ }
54
+ if (message.stopReason === "aborted") return [clock, ...ttft, [`stopped at ${latency(timing.totalMs)}`, 0]];
55
+ if (message.stopReason === "error") return [clock, ...ttft, [`failed at ${latency(timing.totalMs)}`, 0]];
56
+ return [
57
+ clock,
58
+ ...ttft,
59
+ ...(timing.serverMs === undefined ? [] : ([[`server ${latency(timing.serverMs)}`, 6]] as Part[])),
60
+ ...(timing.thoughtMs === undefined ? [] : ([[`thought ${latency(timing.thoughtMs)}`, 5]] as Part[])),
61
+ [`total ${exactly(timing.totalMs)}`, 0],
62
+ ...stall,
63
+ ...rated,
64
+ ];
65
+ }
66
+
67
+ /** Without the provider's path, as `kimi-k2.6` for `@cf/moonshotai/kimi-k2.6`. */
68
+ function modelPart(message: AnsweredMessage): Part[] {
69
+ const name = message.model?.split("/").at(-1);
70
+ return name ? [[name, 1]] : [];
71
+ }
72
+
73
+ interface Settled {
74
+ key: string;
75
+ line: string;
76
+ }
77
+
78
+ /** A finished call's line can't change, and every one of them is asked for on every redraw. */
79
+ const settled = new WeakMap<AnsweredMessage, Settled>();
80
+
81
+ /** What's known of a call to the model, as much of it as fits. */
82
+ export function timingLine(
83
+ timing: Timing | undefined,
84
+ message: AnsweredMessage,
85
+ width: number,
86
+ ): string {
87
+ if (timing?.streaming === true) return buildLine(timing, message, width);
88
+ const key = `${width}|${timing?.at}|${timing?.totalMs}|${timing?.output}|${message.stopReason}|${message.model}`;
89
+ const known = settled.get(message);
90
+ if (known?.key === key) return known.line;
91
+ const line = buildLine(timing, message, width);
92
+ settled.set(message, { key, line });
93
+ return line;
94
+ }
95
+
96
+ function buildLine(timing: Timing | undefined, message: AnsweredMessage, width: number): string {
97
+ const [clock, ...rest] = partsOf(timing, message) as [Part, ...Part[]];
98
+ let parts: Part[] = [clock, ...modelPart(message), ...rest];
99
+ const text = () => parts.map(([part]) => part).join(" ");
100
+ while (visibleWidth(text()) > width) {
101
+ const highest = Math.max(...parts.map(([, drop]) => drop));
102
+ if (highest === 0) break;
103
+ parts = parts.filter(([, drop]) => drop !== highest);
104
+ }
105
+ return text();
106
+ }
@@ -0,0 +1,167 @@
1
+ // The mark follows the icon set in the look's settings.
2
+ import { GLYPH, SPIN } from "../kit/index.ts";
3
+
4
+ import type { IconSet } from "./glyphs.ts";
5
+
6
+ type ToolKind =
7
+ | "shell"
8
+ | "read"
9
+ | "search"
10
+ | "edit"
11
+ | "web"
12
+ | "agent"
13
+ | "ask"
14
+ | "peers"
15
+ | "memory"
16
+ | "mcp"
17
+ | "other";
18
+
19
+ const ICONS: Record<IconSet, Record<ToolKind, string>> = {
20
+ nerd: {
21
+ shell: "\u{f120}", // nf-fa-terminal
22
+ read: "\u{f15c}", // nf-fa-file_lines
23
+ search: "\u{f002}", // nf-fa-search
24
+ edit: "\u{f044}", // nf-fa-pencil
25
+ web: "\u{f0ac}", // nf-fa-globe
26
+ agent: "\u{f06a9}", // nf-md-robot
27
+ ask: "\u{f059}", // nf-fa-circle_question
28
+ peers: "\u{f0c0}", // nf-fa-users
29
+ memory: "\u{f09d1}", // nf-md-brain
30
+ mcp: "\u{f1e6}", // nf-fa-plug
31
+ other: "\u{f013}", // nf-fa-gear
32
+ },
33
+ unicode: {
34
+ shell: GLYPH.pointer,
35
+ read: "\u25a4",
36
+ search: GLYPH.search,
37
+ edit: GLYPH.pencil,
38
+ web: "\u25cd",
39
+ agent: GLYPH.diamond,
40
+ ask: "?",
41
+ peers: "\u2042",
42
+ memory: GLYPH.diamondEmpty,
43
+ mcp: "\u226b",
44
+ other: GLYPH.ring,
45
+ },
46
+ ascii: {
47
+ shell: "$",
48
+ read: "=",
49
+ search: "/",
50
+ edit: "+",
51
+ web: "@",
52
+ agent: "*",
53
+ ask: "?",
54
+ peers: "&",
55
+ memory: "#",
56
+ mcp: ">",
57
+ other: "-",
58
+ },
59
+ };
60
+
61
+ export type CallPhase = "writing" | "waiting" | "running" | "done" | "failed";
62
+
63
+ const MARKS: Record<IconSet, Record<CallPhase, string>> = {
64
+ nerd: {
65
+ writing: "\u{f044}", // nf-fa-pencil
66
+ waiting: "\u{f252}", // nf-fa-hourglass_half
67
+ running: "\u{f017}", // nf-fa-clock_o
68
+ done: "\u{f00c}", // nf-fa-check
69
+ failed: "\u{f00d}", // nf-fa-times
70
+ },
71
+ unicode: {
72
+ writing: GLYPH.pencil,
73
+ waiting: "\u29d6",
74
+ running: "\u25f7",
75
+ done: GLYPH.check,
76
+ failed: GLYPH.cross,
77
+ },
78
+ ascii: { writing: "~", waiting: ":", running: "...", done: "ok", failed: "!" },
79
+ };
80
+
81
+ interface Motion {
82
+ frames: readonly string[];
83
+ stepMs: number;
84
+ }
85
+
86
+ /** A clock outline for every hour, from twelve round to eleven. */
87
+ const CLOCK = [0xf1456, ...Array.from({ length: 11 }, (_, hour) => 0xf144b + hour)].map((code) =>
88
+ String.fromCodePoint(code),
89
+ );
90
+
91
+ const MOTIONS: Record<IconSet, Partial<Record<CallPhase, Motion>>> = {
92
+ nerd: {
93
+ // The same spinner as an agent writing, in every font a Nerd Font is built on.
94
+ writing: { frames: SPIN.braille, stepMs: 100 },
95
+ waiting: { frames: ["\u{f251}", "\u{f252}", "\u{f253}"], stepMs: 400 }, // nf-fa-hourglass_start..end
96
+ running: { frames: CLOCK, stepMs: 110 }, // nf-md-clock_time_*_outline
97
+ },
98
+ unicode: {
99
+ writing: { frames: SPIN.braille, stepMs: 100 },
100
+ waiting: { frames: SPIN.hourglass, stepMs: 500 },
101
+ running: { frames: SPIN.quadrants, stepMs: 150 },
102
+ },
103
+ ascii: {
104
+ running: { frames: SPIN.ascii, stepMs: 150 },
105
+ },
106
+ };
107
+
108
+ /** A phase that lasts moves, so a call still going looks alive. */
109
+ export function stateMark(phase: CallPhase, set: IconSet, now: number = Date.now()): string {
110
+ const motion = MOTIONS[set][phase];
111
+ if (motion === undefined) return MARKS[set][phase];
112
+ return motion.frames[Math.floor(now / motion.stepMs) % motion.frames.length] ?? MARKS[set][phase];
113
+ }
114
+
115
+ export function toolIcon(toolName: string, set: IconSet): string {
116
+ return ICONS[set][toolKind(toolName)];
117
+ }
118
+
119
+ /** An MCP tool says its server rather than `mcp__server__tool`. */
120
+ export function toolLabel(toolName: string): string {
121
+ const mcp = toolName.match(/^mcp__([^_]+)__(.+)$/u);
122
+ return mcp === null ? toolName : `${mcp[1]}:${mcp[2]}`;
123
+ }
124
+
125
+ function toolKind(toolName: string): ToolKind {
126
+ if (toolName.startsWith("mcp__")) return "mcp";
127
+ if (toolName.startsWith("memory_")) return "memory";
128
+ switch (toolName) {
129
+ case "bash":
130
+ case "powershell":
131
+ case "shell":
132
+ return "shell";
133
+ case "read":
134
+ case "ls":
135
+ case "find":
136
+ return "read";
137
+ case "grep":
138
+ case "glob":
139
+ case "tool_search":
140
+ return "search";
141
+ case "edit":
142
+ case "write":
143
+ case "apply_patch":
144
+ return "edit";
145
+ case "web_search":
146
+ case "fetch":
147
+ case "webfetch":
148
+ case "web_fetch":
149
+ case "fetch_content":
150
+ case "get_search_content":
151
+ case "source_check":
152
+ return "web";
153
+ case "task":
154
+ case "agent":
155
+ case "subagent":
156
+ case "bg_wait":
157
+ return "agent";
158
+ case "ask_questions":
159
+ return "ask";
160
+ case "peers":
161
+ return "peers";
162
+ case "scratchpad":
163
+ return "memory";
164
+ default:
165
+ return "other";
166
+ }
167
+ }
@@ -0,0 +1,105 @@
1
+ import { boolean, literal, setting } from "./kit/index.ts";
2
+
3
+ import type { FrameStyle } from "./render/frame.ts";
4
+ import type { IconMode } from "./render/glyphs.ts";
5
+ import type { SegmentId } from "./render/segments.ts";
6
+ import { framePreview, iconsPreview, userFramePreview } from "./ui/previews.ts";
7
+ import type { SlotName } from "./ui/screen.ts";
8
+
9
+ // Only what depends on the terminal or taste is a setting: icons, line style, framed messages.
10
+
11
+ export const SECTIONS = ["Appearance"];
12
+
13
+ export type HeaderStyle = "card" | "compact";
14
+ export type CursorStyle = "block" | "bar" | "underline";
15
+
16
+ const PERMISSION_MODE: SegmentId = "status:pi-ask-permission:mode";
17
+
18
+ /** The line above the editor: how long the agent has worked, and the last call that finished. */
19
+ export const strip: readonly SegmentId[] = ["elapsed"];
20
+ /** The other side of that line, or a line of its own under it when it does not fit. */
21
+ export const stripRight: readonly SegmentId[] = ["board", "peers"];
22
+ /** After the working spinner on the editor's top edge. */
23
+ export const topLeft: readonly SegmentId[] = ["me", "branch", "session"];
24
+ /** Where you are. */
25
+ export const topRight: readonly SegmentId[] = ["path", "host"];
26
+ /** How what you type is handled, and who does the work. */
27
+ export const bottomLeft: readonly SegmentId[] = [PERMISSION_MODE, "model", "effort"];
28
+ /** How full the context is. */
29
+ export const bottomRight: readonly SegmentId[] = ["context"];
30
+ /** What other packages report, the mode first, then the session: cost, tokens, cache, speeds. */
31
+ export const below: readonly SegmentId[] = ["statuses", "cost", "tokens", "cache", "average"];
32
+ /** The plan's quota: what each window spent, and when it resets. */
33
+ export const belowRight: readonly SegmentId[] = ["quota"];
34
+
35
+ /** The docs fixture draws with the same map. */
36
+ export const SLOTS: Record<SlotName, readonly SegmentId[]> = {
37
+ strip,
38
+ stripRight,
39
+ topLeft,
40
+ topRight,
41
+ bottomLeft,
42
+ bottomRight,
43
+ below,
44
+ belowRight,
45
+ };
46
+
47
+ export const SEPARATOR = " ";
48
+ export const cursor: CursorStyle = "bar";
49
+ export const header: HeaderStyle = "card";
50
+ export const pathLength = 40;
51
+ export const gaugeCells = 8;
52
+ export const labels = true;
53
+
54
+ export const icons = setting<IconMode>({
55
+ id: "frame.icons",
56
+ default: "auto",
57
+ decoder: literal("auto", "nerd", "unicode", "ascii"),
58
+ ui: {
59
+ section: "Appearance",
60
+ label: "Icons",
61
+ description: "Auto uses plain Unicode, or ASCII without UTF-8. Pick Nerd if your terminal uses a Nerd Font.",
62
+ preview: iconsPreview,
63
+ },
64
+ });
65
+
66
+ export const frame = setting<FrameStyle | "off">({
67
+ id: "frame.style",
68
+ default: "rounded",
69
+ decoder: literal("rounded", "square", "heavy", "line", "off"),
70
+ project: true,
71
+ ui: {
72
+ section: "Appearance",
73
+ label: "Editor frame",
74
+ description:
75
+ "The lines of the box around the editor. `line` keeps pi's two rules, `off` leaves the editor alone and moves what sits on its edges to the footer.",
76
+ preview: framePreview,
77
+ },
78
+ });
79
+
80
+ export const userFrame = setting({
81
+ id: "frame.userFrame",
82
+ default: true,
83
+ decoder: boolean,
84
+ ui: {
85
+ section: "Appearance",
86
+ label: "Your messages",
87
+ description:
88
+ "Your messages in a frame like the tool calls', named you, with no blank row above and below the text. Off keeps pi's block.",
89
+ preview: userFramePreview,
90
+ },
91
+ });
92
+
93
+ export const timing = setting({
94
+ id: "frame.timing",
95
+ default: true,
96
+ decoder: boolean,
97
+ ui: {
98
+ section: "Appearance",
99
+ label: "Timings",
100
+ description:
101
+ "A line under each response and on each tool box: when it started, to the millisecond, and how long it took.",
102
+ },
103
+ });
104
+
105
+ export const FRAME_SETTINGS = [icons, frame, userFrame, timing];