@visual-z/xeno 0.2.0 → 0.3.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 (259) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +52 -9
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/cli.ts +10 -11
  8. package/src/apps/commands.ts +38 -5
  9. package/src/apps/completions.ts +73 -19
  10. package/src/apps/image-draft.ts +46 -0
  11. package/src/apps/project-trust.ts +2 -42
  12. package/src/apps/provider-connect.ts +346 -0
  13. package/src/apps/render.ts +22 -47
  14. package/src/apps/resources.ts +5 -2
  15. package/src/apps/shell.ts +298 -1526
  16. package/src/apps/startup-input.ts +130 -0
  17. package/src/apps/startup.ts +143 -115
  18. package/src/apps/ui/app.ts +1383 -0
  19. package/src/apps/ui/extension-host.ts +194 -0
  20. package/src/apps/ui/image-cache.ts +78 -0
  21. package/src/apps/ui/palette.ts +88 -0
  22. package/src/apps/ui/project.ts +127 -0
  23. package/src/apps/ui/standalone.ts +146 -0
  24. package/src/apps/ui/status-model.ts +68 -0
  25. package/src/apps/ui/surface.ts +84 -0
  26. package/src/apps/ui/theme.ts +86 -0
  27. package/src/core/acceptance.ts +107 -0
  28. package/src/core/action-defaults.ts +47 -295
  29. package/src/core/benchmark.ts +2 -2
  30. package/src/core/branch-receipt.ts +24 -0
  31. package/src/core/checkpoints.ts +64 -0
  32. package/src/core/churn.ts +2 -0
  33. package/src/core/command-artifacts.ts +94 -0
  34. package/src/core/command-contract.ts +35 -0
  35. package/src/core/completion-review.ts +21 -5
  36. package/src/core/context-detail.ts +37 -0
  37. package/src/core/context-policy.ts +74 -1
  38. package/src/core/continuation.ts +17 -2
  39. package/src/core/derive.ts +29 -3
  40. package/src/core/draft-images.ts +56 -0
  41. package/src/core/environment.ts +145 -0
  42. package/src/core/evaluation-coverage.ts +74 -0
  43. package/src/core/event-validation.ts +80 -5
  44. package/src/core/events.ts +97 -3
  45. package/src/core/evidence-links.ts +7 -0
  46. package/src/core/evidence.ts +13 -3
  47. package/src/core/execution.ts +8 -2
  48. package/src/core/experiment.ts +133 -0
  49. package/src/core/export.ts +9 -0
  50. package/src/core/failure-recovery.ts +173 -0
  51. package/src/core/file-precondition.ts +72 -0
  52. package/src/core/handoff.ts +65 -4
  53. package/src/core/history-query.ts +38 -2
  54. package/src/core/history.ts +38 -1
  55. package/src/core/integration-health.ts +34 -0
  56. package/src/core/keymap.ts +8 -20
  57. package/src/core/model-picker.ts +64 -0
  58. package/src/core/native-eval.ts +25 -0
  59. package/src/core/note-guidance.ts +34 -0
  60. package/src/core/observation-view.ts +49 -8
  61. package/src/core/permissions.ts +24 -2
  62. package/src/core/process-receipt.ts +105 -0
  63. package/src/core/process-state.ts +37 -0
  64. package/src/core/project.ts +45 -18
  65. package/src/core/prompt-policy.ts +37 -0
  66. package/src/core/query-eval-contract.ts +14 -0
  67. package/src/core/read-observation.ts +82 -0
  68. package/src/core/recovery-corpus.ts +17 -0
  69. package/src/core/recovery-evaluation.ts +60 -0
  70. package/src/core/recovery-sources.ts +74 -0
  71. package/src/core/runtime-target.ts +94 -0
  72. package/src/core/search-observation.ts +97 -0
  73. package/src/core/session-tree.ts +147 -0
  74. package/src/core/settings.ts +26 -10
  75. package/src/core/skills.ts +44 -0
  76. package/src/core/sql-query.ts +211 -0
  77. package/src/core/theme.ts +36 -58
  78. package/src/core/tool-contract.ts +51 -2
  79. package/src/core/tool-discovery.ts +87 -0
  80. package/src/core/tool-failure.ts +60 -0
  81. package/src/core/tool-mode-protocol.ts +25 -0
  82. package/src/core/transcript.ts +19 -4
  83. package/src/core/trial-lifecycle.ts +14 -0
  84. package/src/core/tui-mode.ts +29 -0
  85. package/src/core/update-check.ts +97 -0
  86. package/src/core/version.ts +55 -0
  87. package/src/core/work-state.ts +49 -3
  88. package/src/core/working-set.ts +91 -0
  89. package/src/runtime/auth.ts +2 -2
  90. package/src/runtime/branch-jobs.ts +36 -0
  91. package/src/runtime/clipboard.ts +15 -4
  92. package/src/runtime/command-artifacts.ts +64 -0
  93. package/src/runtime/environment-tools.ts +60 -0
  94. package/src/runtime/evidence-notes.ts +4 -2
  95. package/src/runtime/exec-sessions.ts +206 -61
  96. package/src/runtime/execution-control.ts +1 -0
  97. package/src/runtime/extension-ui.ts +22 -30
  98. package/src/runtime/extensions.ts +95 -16
  99. package/src/runtime/harness-comparison.ts +27 -7
  100. package/src/runtime/harness-eval.ts +460 -0
  101. package/src/runtime/history-cache.ts +57 -0
  102. package/src/runtime/history-tools.ts +62 -12
  103. package/src/runtime/integrations.ts +210 -0
  104. package/src/runtime/loop.ts +1021 -268
  105. package/src/runtime/model-discovery.ts +208 -0
  106. package/src/runtime/native-eval-fixtures.ts +33 -0
  107. package/src/runtime/native-eval.ts +143 -0
  108. package/src/runtime/oauth.ts +1 -1
  109. package/src/runtime/provider-connect.ts +109 -0
  110. package/src/runtime/provider-presets.ts +87 -0
  111. package/src/runtime/query-eval-verifier.ts +28 -0
  112. package/src/runtime/recovery-coverage.ts +71 -0
  113. package/src/runtime/recovery-evidence.ts +76 -0
  114. package/src/runtime/recovery-fixtures.ts +116 -0
  115. package/src/runtime/runtime-target.ts +149 -0
  116. package/src/runtime/tool-broker.ts +137 -0
  117. package/src/runtime/tool-discovery.ts +57 -0
  118. package/src/runtime/tool-mode-eval.ts +146 -0
  119. package/src/runtime/tool-queue.ts +77 -0
  120. package/src/runtime/tools.ts +628 -183
  121. package/src/runtime/trial-control.ts +45 -0
  122. package/src/runtime/update-check.ts +47 -0
  123. package/src/runtime/window-trial.ts +2 -1
  124. package/src/runtime/working-notes.ts +33 -3
  125. package/src/runtime/workspace-lifecycle.ts +97 -0
  126. package/src/store/acceptance.ts +15 -0
  127. package/src/store/checkpoint-history.ts +43 -0
  128. package/src/store/command-artifacts.ts +96 -0
  129. package/src/store/command.ts +65 -37
  130. package/src/store/file-checkpoints.ts +104 -0
  131. package/src/store/file-index.ts +13 -1
  132. package/src/store/git-checkpoints.ts +67 -0
  133. package/src/store/observations.ts +61 -20
  134. package/src/store/process-receipts.ts +34 -0
  135. package/src/store/query-source.ts +13 -8
  136. package/src/store/recovery.ts +2 -3
  137. package/src/store/replay.ts +16 -4
  138. package/src/store/runtime-target.ts +64 -0
  139. package/src/store/search.ts +171 -132
  140. package/src/store/session-archive.ts +4 -0
  141. package/src/store/source-cancellation.ts +6 -0
  142. package/src/store/update-cache.ts +36 -0
  143. package/src/store/workspace-observation.ts +113 -0
  144. package/src/tui/ansi-spans.ts +126 -0
  145. package/src/tui/style.ts +19 -16
  146. package/src/tui/ui/atomic-draft.ts +121 -0
  147. package/src/tui/ui/canvas.ts +96 -0
  148. package/src/tui/ui/colors.ts +70 -0
  149. package/src/tui/ui/composer.ts +199 -0
  150. package/src/tui/ui/dialog.ts +379 -0
  151. package/src/tui/ui/draft.ts +23 -0
  152. package/src/tui/ui/edge.ts +186 -0
  153. package/src/tui/ui/image-size.ts +40 -0
  154. package/src/tui/ui/ink.ts +210 -0
  155. package/src/tui/ui/items.ts +40 -0
  156. package/src/tui/ui/keys.ts +40 -0
  157. package/src/tui/ui/layout.ts +74 -0
  158. package/src/tui/ui/list-model.ts +86 -0
  159. package/src/tui/ui/list-view.ts +172 -0
  160. package/src/tui/ui/mascot.ts +67 -0
  161. package/src/tui/ui/messages.ts +518 -0
  162. package/src/tui/ui/motion.ts +28 -0
  163. package/src/tui/ui/paths.ts +10 -0
  164. package/src/tui/ui/pictures.ts +78 -0
  165. package/src/tui/ui/screen.ts +421 -0
  166. package/src/tui/ui/status.ts +271 -0
  167. package/src/tui/ui/styled.ts +85 -0
  168. package/src/tui/ui/testing.ts +5 -0
  169. package/src/tui/ui/tool-card.ts +150 -0
  170. package/src/apps/extension-ui.ts +0 -330
  171. package/src/apps/resource-browser.ts +0 -75
  172. package/src/apps/session-browser.ts +0 -113
  173. package/src/apps/session-picker.ts +0 -55
  174. package/src/tui/ansi.ts +0 -67
  175. package/src/tui/autocomplete.ts +0 -29
  176. package/src/tui/chat-viewport.ts +0 -47
  177. package/src/tui/complete.ts +0 -87
  178. package/src/tui/editor.ts +0 -940
  179. package/src/tui/host.ts +0 -264
  180. package/src/tui/input.ts +0 -75
  181. package/src/tui/keys.ts +0 -378
  182. package/src/tui/markdown.ts +0 -17
  183. package/src/tui/render.ts +0 -153
  184. package/src/tui/select.ts +0 -258
  185. package/src/tui/settings.ts +0 -44
  186. package/src/tui/statusline.ts +0 -162
  187. package/src/tui/system-theme.ts +0 -700
  188. package/src/tui/terminal.ts +0 -30
  189. package/src/tui/text.ts +0 -52
  190. package/src/tui/toolview.ts +0 -386
  191. package/src/tui/transcript.ts +0 -123
  192. package/src/tui/tree.ts +0 -471
  193. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  194. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  195. package/src/vendor/pi-tui/VENDOR.md +0 -25
  196. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  197. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  198. package/src/vendor/pi-tui/colors.ts +0 -367
  199. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  200. package/src/vendor/pi-tui/components/box.ts +0 -166
  201. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  202. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  203. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  204. package/src/vendor/pi-tui/components/image.ts +0 -127
  205. package/src/vendor/pi-tui/components/input.ts +0 -494
  206. package/src/vendor/pi-tui/components/loader.ts +0 -101
  207. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  208. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  209. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  210. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  211. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  212. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  213. package/src/vendor/pi-tui/components/stack.ts +0 -154
  214. package/src/vendor/pi-tui/components/text.ts +0 -107
  215. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  216. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  217. package/src/vendor/pi-tui/editor-component.ts +0 -74
  218. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  219. package/src/vendor/pi-tui/index.ts +0 -186
  220. package/src/vendor/pi-tui/keybindings.ts +0 -320
  221. package/src/vendor/pi-tui/keys.ts +0 -1401
  222. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  223. package/src/vendor/pi-tui/latex.ts +0 -1506
  224. package/src/vendor/pi-tui/layout-node.ts +0 -51
  225. package/src/vendor/pi-tui/layout.ts +0 -449
  226. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  227. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  228. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  229. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  232. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  233. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  234. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  237. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  238. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  239. package/src/vendor/pi-tui/native/napi.h +0 -91
  240. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  241. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  244. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  245. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  246. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  247. package/src/vendor/pi-tui/native-platform.ts +0 -65
  248. package/src/vendor/pi-tui/oklab.ts +0 -233
  249. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  250. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  251. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  252. package/src/vendor/pi-tui/terminal.ts +0 -554
  253. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  254. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  255. package/src/vendor/pi-tui/tui.ts +0 -1493
  256. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  257. package/src/vendor/pi-tui/utils.ts +0 -1385
  258. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  259. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,30 +0,0 @@
1
- /**
2
- * The only thing a renderer can be observed to do is write bytes.
3
- *
4
- * A real tty makes that unassertable -- tests would have to inspect a live
5
- * terminal's screen, and would change its state while doing so. Injecting
6
- * this interface is what makes "a frame that changed nothing writes nothing"
7
- * a unit test rather than a hope.
8
- */
9
-
10
- export interface Terminal {
11
- readonly columns: number;
12
- readonly rows: number;
13
- write(data: string): void;
14
- }
15
-
16
- /**
17
- * A pty opened without TIOCSWINSZ reports `columns === 0`, not
18
- * `undefined`. `n ?? 80` keeps the zero, SelectList's truncate budget
19
- * goes negative, and every label becomes "" -- `/model` shows only `→`.
20
- * Treat non-positive as "size unknown" and fall back to the same 80
21
- * StdoutTerminal already used for a missing ioctl.
22
- */
23
- export function usableColumns(n: number | undefined): number {
24
- return n && n > 0 ? n : 80;
25
- }
26
-
27
- /** Same trap as usableColumns: a 0-row ioctl is not a 0-row screen. */
28
- export function usableRows(n: number | undefined): number {
29
- return n && n > 0 ? n : 24;
30
- }
package/src/tui/text.ts DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * The one component this kernel ships. It exists to prove the renderer is
3
- * actually painting something; Editor / SelectList / Markdown are a later card.
4
- *
5
- * Wrapping is by display width, not by string length: a CJK character that
6
- * would overflow the last column starts a new line instead of being split,
7
- * because a terminal cell cannot hold half of it.
8
- */
9
-
10
- import { displayWidth } from "./width.ts";
11
-
12
- export interface Component {
13
- render(width: number): string[];
14
- }
15
-
16
- export class Text implements Component {
17
- private readonly content: string;
18
-
19
- constructor(content: string) {
20
- this.content = content;
21
- }
22
-
23
- render(width: number): string[] {
24
- const out: string[] = [];
25
- for (const paragraph of this.content.split("\n")) {
26
- out.push(...wrapLine(paragraph, width));
27
- }
28
- return out;
29
- }
30
- }
31
-
32
- function wrapLine(s: string, width: number): string[] {
33
- if (width <= 0) return [s];
34
- const lines: string[] = [];
35
- let current = "";
36
- let currentW = 0;
37
- for (const ch of s) {
38
- const w = displayWidth(ch);
39
- // Combining marks attach to whatever is already in the line; they never
40
- // start a wrap on their own.
41
- if (w > 0 && currentW + w > width && current.length > 0) {
42
- lines.push(current);
43
- current = ch;
44
- currentW = w;
45
- } else {
46
- current += ch;
47
- currentW += w;
48
- }
49
- }
50
- lines.push(current);
51
- return lines;
52
- }
@@ -1,386 +0,0 @@
1
- /**
2
- * Tool-call / tool-result / thinking layout, including collapse.
3
- *
4
- * Pure: renderToolView(event, expanded, width, style) → string[]. Collapse
5
- * is an argument, never log state — the model does not need to know which
6
- * block the user folded (feat-message-rendering design).
7
- *
8
- * LiveTranscript owns the uncommitted tail. Toggle re-renders that tail
9
- * through the line-diff renderer. Committed scrollback is the terminal's
10
- * and is never rewritten; pi's `\x1b[2J\x1b[H\x1b[3J` is refused because
11
- * it wipes history this process did not write.
12
- */
13
-
14
- import { assertNever } from "./exhaustive.ts";
15
- import { renderMarkdown } from "./markdown.ts";
16
- import { renderDiff, emitPrefixed, innerWidth } from "./diff.ts";
17
- import { Renderer, fitLive } from "./render.ts";
18
- import {
19
- ANSI,
20
- PLAIN_ATTRS,
21
- type Attrs,
22
- type Style,
23
- } from "./style.ts";
24
- import { usableColumns, usableRows, type Terminal } from "./terminal.ts";
25
- import { CR, ERASE_DOWN, cursorUp } from "./ansi.ts";
26
-
27
- /**
28
- * Lines shown before a collapsed tool body. From pi's write renderer (10).
29
- * Assumed, not measured: a taller preview fills the live region before
30
- * the user can hit Ctrl+O, and a shorter one hides too much of a typical
31
- * command's first useful lines.
32
- */
33
- const COLLAPSED_PREVIEW_LINES = 10;
34
-
35
- const DIM: Attrs = { ...PLAIN_ATTRS, dim: true, result: true };
36
- const CYAN: Attrs = { ...PLAIN_ATTRS, tool: true };
37
- const RED: Attrs = { ...PLAIN_ATTRS, error: true };
38
- const THINKING: Attrs = { ...PLAIN_ATTRS, dim: true, thinking: true };
39
-
40
- function withThinking(base: Style): Style {
41
- return {
42
- open(attrs) {
43
- return base.open({ ...attrs, thinking: true, dim: true });
44
- },
45
- close(attrs) {
46
- return base.close({ ...attrs, thinking: true, dim: true });
47
- },
48
- };
49
- }
50
-
51
- export type ToolViewEvent =
52
- | { kind: "thinking"; text: string }
53
- | { kind: "tool_call"; tool: { name: string; input: unknown } }
54
- | {
55
- kind: "tool_result";
56
- text: string;
57
- ok: boolean;
58
- tool?: { name: string; input: unknown };
59
- };
60
-
61
- export function renderToolView(
62
- event: ToolViewEvent,
63
- expanded: boolean,
64
- width: number,
65
- style: Style,
66
- ): string[] {
67
- const w = Math.max(0, Math.floor(width));
68
- switch (event.kind) {
69
- case "thinking":
70
- return renderThinking(event.text, expanded, w, style);
71
- case "tool_call":
72
- return renderCall(event.tool, expanded, w, style);
73
- case "tool_result":
74
- return renderResult(event, expanded, w, style);
75
- default:
76
- return assertNever(event, "renderToolView");
77
- }
78
- }
79
-
80
- function renderThinking(text: string, expanded: boolean, width: number, style: Style): string[] {
81
- if (width <= 0) return [""];
82
- const indent = " ";
83
- if (expanded) {
84
- // Wrap to the same inner width emitPrefixed would paint into. A
85
- // floor of 1 plus a two-space indent is three columns in a
86
- // one-column terminal — the overflow this card exists to close.
87
- const inner = innerWidth(indent, width);
88
- // CommonMark folds a single newline into a space. The previous subset
89
- // parser did that; pi's marked keeps the break. Folding here keeps the
90
- // tool-view snapshot and the wrap of a thinking paragraph stable.
91
- const folded = text.replace(/\r\n/g, "\n").replace(/(?<!\n)\n(?!\n)/g, " ");
92
- const md = renderMarkdown(folded, inner, withThinking(style));
93
- if (inner === width) return md;
94
- return md.map((line) => `${indent}${line}`);
95
- }
96
- const source = text.replace(/\r\n/g, "\n").split("\n");
97
- const first = source[0] ?? "";
98
- const hidden = Math.max(0, source.length - 1);
99
- const out = emitPrefixed(indent, first, width, style, THINKING);
100
- if (hidden > 0) {
101
- out.push(...emitPrefixed(indent, `… ${hidden} more lines`, width, style, DIM));
102
- }
103
- return out;
104
- }
105
-
106
- function renderCall(
107
- tool: { name: string; input: unknown },
108
- expanded: boolean,
109
- width: number,
110
- style: Style,
111
- ): string[] {
112
- const input = asRecord(tool.input);
113
- const name = tool.name;
114
- switch (name) {
115
- case "edit":
116
- return renderEditCall(input, expanded, width, style);
117
- case "write":
118
- return renderWriteCall(input, expanded, width, style);
119
- case "bash":
120
- return renderBashCall(input, width, style);
121
- case "read":
122
- return renderReadCall(input, width, style);
123
- default:
124
- return emitPrefixed(" → ", headerTail(name, input), width, style, CYAN);
125
- }
126
- }
127
-
128
- function renderEditCall(
129
- input: Record<string, unknown>,
130
- expanded: boolean,
131
- width: number,
132
- style: Style,
133
- ): string[] {
134
- const path = str(input["path"]);
135
- // old/new are the names logs written before old_string/new_string carry.
136
- const edits = Array.isArray(input["edits"]) ? input["edits"] as Record<string, unknown>[] : [input];
137
- const oldText = edits.map((edit) => str(edit["old_string"] ?? edit["old"])).join("\n");
138
- const newText = edits.map((edit) => str(edit["new_string"] ?? edit["new"])).join("\n");
139
- const out = emitPrefixed(" → ", `edit ${path}${edits.length > 1 ? ` (${edits.length} edits)` : ""}`, width, style, CYAN);
140
- if (oldText.length === 0 && newText.length === 0) return out;
141
- const diff = renderDiff(oldText, newText, width, style);
142
- return out.concat(collapse(diff, expanded, COLLAPSED_PREVIEW_LINES, width, style));
143
- }
144
-
145
- function renderWriteCall(
146
- input: Record<string, unknown>,
147
- expanded: boolean,
148
- width: number,
149
- style: Style,
150
- ): string[] {
151
- const path = str(input["path"]);
152
- const content = str(input["content"]);
153
- const out = emitPrefixed(" → ", `write ${path}`, width, style, CYAN);
154
- if (content.length === 0) return out;
155
- const body = content.split("\n").flatMap((line) => emitPrefixed(" ", line, width, style, DIM));
156
- return out.concat(collapse(body, expanded, COLLAPSED_PREVIEW_LINES, width, style));
157
- }
158
-
159
- function renderBashCall(input: Record<string, unknown>, width: number, style: Style): string[] {
160
- const command = str(input["command"]);
161
- return emitPrefixed(" → ", `bash ${command}`, width, style, CYAN);
162
- }
163
-
164
- function renderReadCall(input: Record<string, unknown>, width: number, style: Style): string[] {
165
- const path = str(input["path"]);
166
- const range = formatReadRange(input);
167
- return emitPrefixed(" → ", `read ${path}${range}`, width, style, CYAN);
168
- }
169
-
170
- function renderResult(
171
- event: { text: string; ok: boolean; tool?: { name: string; input: unknown } },
172
- expanded: boolean,
173
- width: number,
174
- style: Style,
175
- ): string[] {
176
- const name = event.tool?.name ?? inferToolName(event.text);
177
- const ok = event.ok;
178
- switch (name) {
179
- case "bash":
180
- return renderBashResult(event.text, ok, expanded, width, style);
181
- case "read":
182
- return renderReadResult(event.text, ok, width, style);
183
- case "write":
184
- case "edit":
185
- return renderStatusResult(event.text, ok, width, style);
186
- default:
187
- return renderGenericResult(event.text, ok, expanded, width, style);
188
- }
189
- }
190
-
191
- function renderBashResult(
192
- text: string,
193
- ok: boolean,
194
- expanded: boolean,
195
- width: number,
196
- style: Style,
197
- ): string[] {
198
- const parsed = parseBashResult(text);
199
- const exitLabel = parsed.label;
200
- const mark = ok ? " ok " : " err ";
201
- const markAttrs = ok ? DIM : RED;
202
- const header = exitLabel.length > 0 ? mark + exitLabel : mark.trimEnd();
203
- const out = emitPrefixed("", header, width, style, markAttrs);
204
-
205
- const body = parsed.body;
206
- if (body.length === 0) return out;
207
- const stream = ok ? "stdout" : "stderr";
208
- const bodyAttrs: Attrs = ok ? DIM : RED;
209
- const lines = body.split("\n").flatMap((line) =>
210
- emitPrefixed(` ${stream} `, line, width, style, bodyAttrs),
211
- );
212
- return out.concat(collapse(lines, expanded, COLLAPSED_PREVIEW_LINES, width, style));
213
- }
214
-
215
- function renderReadResult(text: string, ok: boolean, width: number, style: Style): string[] {
216
- if (!ok) return renderStatusResult(text, false, width, style);
217
- const n = text.length === 0 ? 0 : text.split("\n").length;
218
- return emitPrefixed(" ", `${n} lines`, width, style, DIM);
219
- }
220
-
221
- function renderStatusResult(text: string, ok: boolean, width: number, style: Style): string[] {
222
- const mark = ok ? " ok " : " err ";
223
- const first = text.split("\n")[0] ?? "";
224
- return emitPrefixed(mark, first, width, style, ok ? DIM : RED);
225
- }
226
-
227
- function renderGenericResult(
228
- text: string,
229
- ok: boolean,
230
- expanded: boolean,
231
- width: number,
232
- style: Style,
233
- ): string[] {
234
- const mark = ok ? " ok " : " err ";
235
- const lines = text.split("\n");
236
- const first = emitPrefixed(mark, lines[0] ?? "", width, style, ok ? DIM : RED);
237
- const rest = lines.slice(1).flatMap((line) => emitPrefixed(" ", line, width, style, DIM));
238
- return first.concat(collapse(rest, expanded, COLLAPSED_PREVIEW_LINES, width, style));
239
- }
240
-
241
- function collapse(
242
- lines: readonly string[],
243
- expanded: boolean,
244
- preview: number,
245
- width: number,
246
- style: Style,
247
- ): string[] {
248
- if (expanded || lines.length <= preview) return [...lines];
249
- const shown = lines.slice(0, preview);
250
- const hidden = lines.length - preview;
251
- const note = paintRemainder(hidden, width, style);
252
- return [...shown, ...note];
253
- }
254
-
255
- function paintRemainder(hidden: number, width: number, style: Style): string[] {
256
- return emitPrefixed(" ", `… ${hidden} more lines`, width, style, DIM);
257
- }
258
-
259
- /** The header is `exit N` optionally followed by ` · wall · status`; all of it is the label. */
260
- function parseBashResult(text: string): { exit: number | null; label: string; body: string } {
261
- const m = /^exit (\d+)([^\n]*)\n?/.exec(text);
262
- if (!m) return { exit: null, label: "", body: text };
263
- return { exit: Number(m[1]), label: `exit ${m[1]}${m[2] ?? ""}`, body: text.slice(m[0].length) };
264
- }
265
-
266
- function inferToolName(text: string): string {
267
- if (/^exit \d+/.test(text)) return "bash";
268
- if (/^wrote \d+ bytes/.test(text)) return "write";
269
- if (/^replaced /.test(text)) return "edit";
270
- if (/^\d+\t/.test(text)) return "read";
271
- return "";
272
- }
273
-
274
- function formatReadRange(input: Record<string, unknown>): string {
275
- const start = input["start"];
276
- const end = input["end"];
277
- if (typeof start !== "number" && typeof end !== "number") return "";
278
- const a = typeof start === "number" ? start : 1;
279
- const b = typeof end === "number" ? String(end) : "";
280
- return b.length > 0 ? `:${a}-${b}` : `:${a}`;
281
- }
282
-
283
- function headerTail(name: string, input: Record<string, unknown>): string {
284
- const path = str(input["path"]);
285
- const command = str(input["command"]);
286
- if (path.length > 0) return `${name} ${path}`;
287
- if (command.length > 0) return `${name} ${command}`;
288
- return name;
289
- }
290
-
291
- function asRecord(input: unknown): Record<string, unknown> {
292
- if (typeof input === "object" && input !== null) return input as Record<string, unknown>;
293
- return {};
294
- }
295
-
296
- function str(v: unknown): string {
297
- return typeof v === "string" ? v : "";
298
- }
299
-
300
- /**
301
- * Uncommitted tool/thinking lines, plus commit/toggle against a terminal.
302
- *
303
- * Toggle only asks the line-diff renderer to rewrite the live tail. Overflow
304
- * already written as scrollback is not reprinted, and the screen is never
305
- * cleared.
306
- */
307
- export class LiveTranscript {
308
- toolsExpanded = false;
309
- thinkingExpanded = false;
310
- private pending: ToolViewEvent[] = [];
311
- private flushed = 0;
312
- private renderer: Renderer;
313
- private liveRows = 0;
314
- private readonly terminal: Terminal;
315
- private readonly style: Style;
316
-
317
- constructor(terminal: Terminal, style: Style = ANSI) {
318
- this.terminal = terminal;
319
- this.style = style;
320
- this.renderer = new Renderer(terminal);
321
- }
322
-
323
- push(event: ToolViewEvent): void {
324
- this.pending.push(event);
325
- this.paint();
326
- }
327
-
328
- commit(): void {
329
- const lines = this.pendingLines();
330
- const leftover = lines.slice(this.flushed);
331
- this.eraseLive();
332
- if (leftover.length > 0) this.terminal.write(leftover.join("\n") + "\n");
333
- this.pending = [];
334
- this.flushed = 0;
335
- this.renderer = new Renderer(this.terminal);
336
- this.liveRows = 0;
337
- }
338
-
339
- toggleTools(): void {
340
- this.toolsExpanded = !this.toolsExpanded;
341
- this.paint();
342
- }
343
-
344
- toggleThinking(): void {
345
- this.thinkingExpanded = !this.thinkingExpanded;
346
- this.paint();
347
- }
348
-
349
- private expandedFor(event: ToolViewEvent): boolean {
350
- return event.kind === "thinking" ? this.thinkingExpanded : this.toolsExpanded;
351
- }
352
-
353
- private pendingLines(): string[] {
354
- const width = usableColumns(this.terminal.columns);
355
- return this.pending.flatMap((event) =>
356
- renderToolView(event, this.expandedFor(event), width, this.style),
357
- );
358
- }
359
-
360
- private paint(): void {
361
- const rows = usableRows(this.terminal.rows);
362
- const lines = this.pendingLines();
363
- const fitted = fitLive(lines, rows);
364
- if (fitted.overflow.length > this.flushed) {
365
- const fresh = fitted.overflow.slice(this.flushed);
366
- this.eraseLive();
367
- this.terminal.write(fresh.join("\n") + "\n");
368
- this.flushed = fitted.overflow.length;
369
- this.renderer = new Renderer(this.terminal);
370
- this.liveRows = 0;
371
- }
372
- this.renderer.render({ render: () => fitted.live });
373
- this.liveRows = this.renderer.liveRowCount;
374
- }
375
-
376
- private eraseLive(): void {
377
- if (this.liveRows <= 0 && this.renderer.liveRowCount <= 0) return;
378
- const rows = Math.max(this.liveRows, this.renderer.liveRowCount);
379
- let out = CR;
380
- if (rows > 1) out += cursorUp(rows - 1);
381
- out += ERASE_DOWN;
382
- this.terminal.write(out);
383
- this.renderer = new Renderer(this.terminal);
384
- this.liveRows = 0;
385
- }
386
- }
@@ -1,123 +0,0 @@
1
- import { Container, PiText, Lines, assistantMarkdown, type MarkdownFeatures } from "./host.ts";
2
- import { Image } from "../vendor/pi-tui/components/image.ts";
3
- import { renderToolView } from "./toolview.ts";
4
- import { PLAIN_ATTRS, paint, type Style } from "./style.ts";
5
- import { applyBackgroundToLine } from "../vendor/pi-tui/utils.ts";
6
- import type { Component } from "../vendor/pi-tui/tui.ts";
7
-
8
- /** Presentation DTO; the application supplies its pure log projection. */
9
- type Attachment = { sha256: string; name: string; width: number; height: number };
10
- type Item = { id: string; revision: string } & (
11
- | { kind: "user"; text: string; interrupted: boolean; images?: readonly Attachment[] }
12
- | { kind: "assistant" | "thinking" | "notice"; text: string }
13
- | { kind: "custom"; text: string; customType: string; data?: unknown; entry: boolean; timestamp: string }
14
- | { kind: "tool"; callId: string; name: string; input: unknown; result: { text: string; ok: boolean; data?: unknown; images?: readonly Attachment[] } | null }
15
- );
16
- interface ExtensionRenderers {
17
- message(type: string, text: string, data: unknown, expanded: boolean, fallback: Component, metadata: { id: string; entry: boolean; timestamp: string }): Component;
18
- tool(name: string, input: unknown, result: { text: string; ok: boolean; data?: unknown } | null, expanded: boolean, call: Component, output: Component, id: string): Component;
19
- hiddenThinkingLabel: string | undefined;
20
- }
21
- function dispose(component: Component): void {
22
- if (component instanceof Container) component.children.forEach(dispose);
23
- else if ("dispose" in component && typeof component.dispose === "function") component.dispose();
24
- }
25
-
26
- export class Transcript extends Container {
27
- private items: readonly Item[] = [];
28
- private cache = new Map<string, { revision: string; component: Container }>();
29
- private style: Style | null = null;
30
- private toolsExpanded = false;
31
- private thinkingExpanded = false;
32
- private padding = 1;
33
- private features: MarkdownFeatures | undefined;
34
- private images: ReadonlyMap<string, string> = new Map();
35
- private showImages = true;
36
- private imageWidth = 60;
37
- private extensions: ExtensionRenderers | undefined;
38
- setExtensions(renderers: ExtensionRenderers): void { this.extensions = renderers; }
39
- private clearCache(): void { for (const entry of this.cache.values()) dispose(entry.component); this.cache.clear(); }
40
- configure(features: MarkdownFeatures, images: ReadonlyMap<string, string>, showImages: boolean, width: number): void {
41
- this.features = features; this.images = images; this.showImages = showImages; this.imageWidth = width;
42
- this.clearCache(); this.items = [];
43
- }
44
- setPadding(padding: number): void { if (this.padding !== padding) { this.padding = padding; this.invalidate(); } }
45
- override render(width: number): string[] {
46
- const padding = Math.min(this.padding, Math.max(0, Math.floor((width - 1) / 2)));
47
- return super.render(Math.max(1, width - padding * 2)).map(line => line.startsWith("\x1b]133;A\x07")
48
- ? "\x1b]133;A\x07" + " ".repeat(padding) + line.slice(8) : " ".repeat(padding) + line);
49
- }
50
-
51
- update(items: readonly Item[], style: Style, toolsExpanded: boolean, thinkingExpanded: boolean): void {
52
- const changed = style !== this.style || toolsExpanded !== this.toolsExpanded || thinkingExpanded !== this.thinkingExpanded;
53
- if (!changed && this.items === items) return;
54
- if (changed) this.clearCache();
55
- this.items = items;
56
- this.style = style;
57
- this.toolsExpanded = toolsExpanded;
58
- this.thinkingExpanded = thinkingExpanded;
59
- const next = new Map<string, { revision: string; component: Container }>();
60
- this.clear();
61
- for (const item of items) {
62
- const old = this.cache.get(item.id);
63
- const component = old?.revision === item.revision ? old.component : this.component(item, style);
64
- next.set(item.id, { revision: item.revision, component });
65
- this.addChild(component);
66
- }
67
- for (const [id, entry] of this.cache) if (next.get(id)?.component !== entry.component) dispose(entry.component);
68
- this.cache = next;
69
- }
70
-
71
- private component(item: Item, style: Style): Container {
72
- const component = new Container();
73
- const dim = (text: string) => paint(text, { ...PLAIN_ATTRS, dim: true }, style);
74
- switch (item.kind) {
75
- case "user": {
76
- component.addChild(new Lines(width => {
77
- const transformed = this.features?.transform(item.text, width, false, style, "user") ?? item.text;
78
- const text = new PiText(paint(`❯ ${transformed}`, { ...PLAIN_ATTRS, prompt: true }, style), 0, 1, text => style.token?.("userMessageBg", text) ?? text);
79
- return text.render(width).map((line, index) => index === 0 ? "\x1b]133;A\x07" + line : line);
80
- }));
81
- this.addImages(component, item.images, dim);
82
- if (item.interrupted) component.addChild(new PiText(dim(" interrupted"), 0, 0));
83
- break;
84
- }
85
- case "assistant": component.addChild(assistantMarkdown(item.text, style, this.features)); break;
86
- case "thinking": {
87
- const expanded = this.thinkingExpanded;
88
- component.addChild(!expanded && this.extensions?.hiddenThinkingLabel !== undefined ? new PiText(this.extensions.hiddenThinkingLabel, 0, 0) : new Lines((width) => renderToolView({ kind: "thinking", text: this.features?.transform(item.text, width, false, style, "assistant-thinking") ?? item.text }, expanded, width, style)));
89
- break;
90
- }
91
- case "tool": {
92
- const tool = { name: item.name, input: item.input };
93
- const expanded = this.toolsExpanded;
94
- const background = item.result === null ? "toolPendingBg" : item.result.ok ? "toolSuccessBg" : "toolErrorBg";
95
- const rows: Lines[] = [new Lines((width) => renderToolView({ kind: "tool_call", tool }, expanded, width, style))];
96
- if (item.result) {
97
- const result = item.result;
98
- rows.push(new Lines((width) => renderToolView({ kind: "tool_result", tool, text: result.text, ok: result.ok }, expanded, width, style)));
99
- } else rows.push(new Lines(() => [dim(" pending")]));
100
- const rendered = this.extensions?.tool(item.name, item.input, item.result, expanded, rows[0]!, rows[1]!, item.callId) ?? new Lines(width => rows.flatMap(row => row.render(width)));
101
- const toolComponent = { render: (width: number) => rendered.render(width).map(line => applyBackgroundToLine(line, width, text => style.token?.(background, text) ?? text)),
102
- invalidate: () => rendered.invalidate(), dispose: () => dispose(rendered) };
103
- component.addChild(toolComponent);
104
- this.addImages(component, item.result?.images, dim);
105
- break;
106
- }
107
- case "notice": component.addChild(new PiText(dim(item.text), 0, 0)); break;
108
- case "custom": {
109
- const fallback = new PiText(dim(`[${item.customType}] ${item.text}`), 0, 0);
110
- component.addChild(this.extensions?.message(item.customType, item.text, item.data, this.toolsExpanded, fallback, item) ?? fallback); break;
111
- }
112
- }
113
- return component;
114
- }
115
- private addImages(component: Container, images: readonly Attachment[] | undefined, dim: (text: string) => string): void {
116
- for (const image of images ?? []) {
117
- const data = this.images.get(image.sha256);
118
- if (this.showImages && data) component.addChild(new Image(data, "image/png", { fallbackColor: dim },
119
- { filename: image.name, maxWidthCells: this.imageWidth }, { widthPx: image.width, heightPx: image.height }));
120
- else component.addChild(new PiText(dim(`[Image: ${image.name}, ${image.width}x${image.height}]`), 0, 0));
121
- }
122
- }
123
- }