@visual-z/xeno 0.2.1 → 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 (258) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +51 -13
  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/commands.ts +38 -5
  8. package/src/apps/completions.ts +73 -19
  9. package/src/apps/image-draft.ts +46 -0
  10. package/src/apps/project-trust.ts +2 -42
  11. package/src/apps/provider-connect.ts +346 -0
  12. package/src/apps/render.ts +22 -47
  13. package/src/apps/resources.ts +5 -2
  14. package/src/apps/shell.ts +298 -1526
  15. package/src/apps/startup-input.ts +45 -9
  16. package/src/apps/startup.ts +128 -86
  17. package/src/apps/ui/app.ts +1383 -0
  18. package/src/apps/ui/extension-host.ts +194 -0
  19. package/src/apps/ui/image-cache.ts +78 -0
  20. package/src/apps/ui/palette.ts +88 -0
  21. package/src/apps/ui/project.ts +127 -0
  22. package/src/apps/ui/standalone.ts +146 -0
  23. package/src/apps/ui/status-model.ts +68 -0
  24. package/src/apps/ui/surface.ts +84 -0
  25. package/src/apps/ui/theme.ts +86 -0
  26. package/src/core/acceptance.ts +107 -0
  27. package/src/core/action-defaults.ts +47 -295
  28. package/src/core/benchmark.ts +2 -2
  29. package/src/core/branch-receipt.ts +24 -0
  30. package/src/core/checkpoints.ts +64 -0
  31. package/src/core/churn.ts +2 -0
  32. package/src/core/command-artifacts.ts +94 -0
  33. package/src/core/command-contract.ts +35 -0
  34. package/src/core/completion-review.ts +21 -5
  35. package/src/core/context-detail.ts +37 -0
  36. package/src/core/context-policy.ts +74 -1
  37. package/src/core/continuation.ts +17 -2
  38. package/src/core/derive.ts +29 -3
  39. package/src/core/draft-images.ts +56 -0
  40. package/src/core/environment.ts +145 -0
  41. package/src/core/evaluation-coverage.ts +74 -0
  42. package/src/core/event-validation.ts +80 -5
  43. package/src/core/events.ts +97 -3
  44. package/src/core/evidence-links.ts +7 -0
  45. package/src/core/evidence.ts +13 -3
  46. package/src/core/execution.ts +8 -2
  47. package/src/core/experiment.ts +133 -0
  48. package/src/core/export.ts +9 -0
  49. package/src/core/failure-recovery.ts +173 -0
  50. package/src/core/file-precondition.ts +72 -0
  51. package/src/core/handoff.ts +65 -4
  52. package/src/core/history-query.ts +38 -2
  53. package/src/core/history.ts +38 -1
  54. package/src/core/integration-health.ts +34 -0
  55. package/src/core/keymap.ts +8 -20
  56. package/src/core/model-picker.ts +64 -0
  57. package/src/core/native-eval.ts +25 -0
  58. package/src/core/note-guidance.ts +34 -0
  59. package/src/core/observation-view.ts +49 -8
  60. package/src/core/permissions.ts +24 -2
  61. package/src/core/process-receipt.ts +105 -0
  62. package/src/core/process-state.ts +37 -0
  63. package/src/core/project.ts +45 -18
  64. package/src/core/prompt-policy.ts +37 -0
  65. package/src/core/query-eval-contract.ts +14 -0
  66. package/src/core/read-observation.ts +82 -0
  67. package/src/core/recovery-corpus.ts +17 -0
  68. package/src/core/recovery-evaluation.ts +60 -0
  69. package/src/core/recovery-sources.ts +74 -0
  70. package/src/core/runtime-target.ts +94 -0
  71. package/src/core/search-observation.ts +97 -0
  72. package/src/core/session-tree.ts +147 -0
  73. package/src/core/settings.ts +26 -10
  74. package/src/core/skills.ts +44 -0
  75. package/src/core/sql-query.ts +211 -0
  76. package/src/core/theme.ts +36 -58
  77. package/src/core/tool-contract.ts +51 -2
  78. package/src/core/tool-discovery.ts +87 -0
  79. package/src/core/tool-failure.ts +60 -0
  80. package/src/core/tool-mode-protocol.ts +25 -0
  81. package/src/core/transcript.ts +19 -4
  82. package/src/core/trial-lifecycle.ts +14 -0
  83. package/src/core/tui-mode.ts +29 -0
  84. package/src/core/update-check.ts +97 -0
  85. package/src/core/version.ts +55 -0
  86. package/src/core/work-state.ts +49 -3
  87. package/src/core/working-set.ts +91 -0
  88. package/src/runtime/auth.ts +2 -2
  89. package/src/runtime/branch-jobs.ts +36 -0
  90. package/src/runtime/clipboard.ts +15 -4
  91. package/src/runtime/command-artifacts.ts +64 -0
  92. package/src/runtime/environment-tools.ts +60 -0
  93. package/src/runtime/evidence-notes.ts +4 -2
  94. package/src/runtime/exec-sessions.ts +206 -61
  95. package/src/runtime/execution-control.ts +1 -0
  96. package/src/runtime/extension-ui.ts +22 -30
  97. package/src/runtime/extensions.ts +95 -16
  98. package/src/runtime/harness-comparison.ts +27 -7
  99. package/src/runtime/harness-eval.ts +460 -0
  100. package/src/runtime/history-cache.ts +57 -0
  101. package/src/runtime/history-tools.ts +62 -12
  102. package/src/runtime/integrations.ts +210 -0
  103. package/src/runtime/loop.ts +1021 -268
  104. package/src/runtime/model-discovery.ts +208 -0
  105. package/src/runtime/native-eval-fixtures.ts +33 -0
  106. package/src/runtime/native-eval.ts +143 -0
  107. package/src/runtime/oauth.ts +1 -1
  108. package/src/runtime/provider-connect.ts +109 -0
  109. package/src/runtime/provider-presets.ts +87 -0
  110. package/src/runtime/query-eval-verifier.ts +28 -0
  111. package/src/runtime/recovery-coverage.ts +71 -0
  112. package/src/runtime/recovery-evidence.ts +76 -0
  113. package/src/runtime/recovery-fixtures.ts +116 -0
  114. package/src/runtime/runtime-target.ts +149 -0
  115. package/src/runtime/tool-broker.ts +137 -0
  116. package/src/runtime/tool-discovery.ts +57 -0
  117. package/src/runtime/tool-mode-eval.ts +146 -0
  118. package/src/runtime/tool-queue.ts +77 -0
  119. package/src/runtime/tools.ts +628 -183
  120. package/src/runtime/trial-control.ts +45 -0
  121. package/src/runtime/update-check.ts +47 -0
  122. package/src/runtime/window-trial.ts +2 -1
  123. package/src/runtime/working-notes.ts +33 -3
  124. package/src/runtime/workspace-lifecycle.ts +97 -0
  125. package/src/store/acceptance.ts +15 -0
  126. package/src/store/checkpoint-history.ts +43 -0
  127. package/src/store/command-artifacts.ts +96 -0
  128. package/src/store/command.ts +65 -37
  129. package/src/store/file-checkpoints.ts +104 -0
  130. package/src/store/file-index.ts +13 -1
  131. package/src/store/git-checkpoints.ts +67 -0
  132. package/src/store/observations.ts +61 -20
  133. package/src/store/process-receipts.ts +34 -0
  134. package/src/store/query-source.ts +13 -8
  135. package/src/store/recovery.ts +2 -3
  136. package/src/store/replay.ts +16 -4
  137. package/src/store/runtime-target.ts +64 -0
  138. package/src/store/search.ts +171 -132
  139. package/src/store/session-archive.ts +4 -0
  140. package/src/store/source-cancellation.ts +6 -0
  141. package/src/store/update-cache.ts +36 -0
  142. package/src/store/workspace-observation.ts +113 -0
  143. package/src/tui/ansi-spans.ts +126 -0
  144. package/src/tui/style.ts +19 -16
  145. package/src/tui/ui/atomic-draft.ts +121 -0
  146. package/src/tui/ui/canvas.ts +96 -0
  147. package/src/tui/ui/colors.ts +70 -0
  148. package/src/tui/ui/composer.ts +199 -0
  149. package/src/tui/ui/dialog.ts +379 -0
  150. package/src/tui/ui/draft.ts +23 -0
  151. package/src/tui/ui/edge.ts +186 -0
  152. package/src/tui/ui/image-size.ts +40 -0
  153. package/src/tui/ui/ink.ts +210 -0
  154. package/src/tui/ui/items.ts +40 -0
  155. package/src/tui/ui/keys.ts +40 -0
  156. package/src/tui/ui/layout.ts +74 -0
  157. package/src/tui/ui/list-model.ts +86 -0
  158. package/src/tui/ui/list-view.ts +172 -0
  159. package/src/tui/ui/mascot.ts +67 -0
  160. package/src/tui/ui/messages.ts +518 -0
  161. package/src/tui/ui/motion.ts +28 -0
  162. package/src/tui/ui/paths.ts +10 -0
  163. package/src/tui/ui/pictures.ts +78 -0
  164. package/src/tui/ui/screen.ts +421 -0
  165. package/src/tui/ui/status.ts +271 -0
  166. package/src/tui/ui/styled.ts +85 -0
  167. package/src/tui/ui/testing.ts +5 -0
  168. package/src/tui/ui/tool-card.ts +150 -0
  169. package/src/apps/extension-ui.ts +0 -330
  170. package/src/apps/resource-browser.ts +0 -75
  171. package/src/apps/session-browser.ts +0 -113
  172. package/src/apps/session-picker.ts +0 -55
  173. package/src/tui/ansi.ts +0 -67
  174. package/src/tui/autocomplete.ts +0 -29
  175. package/src/tui/chat-viewport.ts +0 -47
  176. package/src/tui/complete.ts +0 -87
  177. package/src/tui/editor.ts +0 -940
  178. package/src/tui/host.ts +0 -264
  179. package/src/tui/input.ts +0 -75
  180. package/src/tui/keys.ts +0 -378
  181. package/src/tui/markdown.ts +0 -17
  182. package/src/tui/render.ts +0 -153
  183. package/src/tui/select.ts +0 -258
  184. package/src/tui/settings.ts +0 -44
  185. package/src/tui/statusline.ts +0 -162
  186. package/src/tui/system-theme.ts +0 -700
  187. package/src/tui/terminal.ts +0 -30
  188. package/src/tui/text.ts +0 -52
  189. package/src/tui/toolview.ts +0 -386
  190. package/src/tui/transcript.ts +0 -123
  191. package/src/tui/tree.ts +0 -471
  192. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  193. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  194. package/src/vendor/pi-tui/VENDOR.md +0 -25
  195. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  196. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  197. package/src/vendor/pi-tui/colors.ts +0 -367
  198. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  199. package/src/vendor/pi-tui/components/box.ts +0 -166
  200. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  201. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  202. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  203. package/src/vendor/pi-tui/components/image.ts +0 -127
  204. package/src/vendor/pi-tui/components/input.ts +0 -494
  205. package/src/vendor/pi-tui/components/loader.ts +0 -101
  206. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  207. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  208. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  209. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  210. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  211. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  212. package/src/vendor/pi-tui/components/stack.ts +0 -154
  213. package/src/vendor/pi-tui/components/text.ts +0 -107
  214. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  215. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  216. package/src/vendor/pi-tui/editor-component.ts +0 -74
  217. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  218. package/src/vendor/pi-tui/index.ts +0 -186
  219. package/src/vendor/pi-tui/keybindings.ts +0 -320
  220. package/src/vendor/pi-tui/keys.ts +0 -1401
  221. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  222. package/src/vendor/pi-tui/latex.ts +0 -1506
  223. package/src/vendor/pi-tui/layout-node.ts +0 -51
  224. package/src/vendor/pi-tui/layout.ts +0 -449
  225. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  226. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  227. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  228. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  229. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  232. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  233. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  234. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  237. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  238. package/src/vendor/pi-tui/native/napi.h +0 -91
  239. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  240. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  241. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  244. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  245. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  246. package/src/vendor/pi-tui/native-platform.ts +0 -65
  247. package/src/vendor/pi-tui/oklab.ts +0 -233
  248. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  249. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  250. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  251. package/src/vendor/pi-tui/terminal.ts +0 -554
  252. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  253. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  254. package/src/vendor/pi-tui/tui.ts +0 -1493
  255. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  256. package/src/vendor/pi-tui/utils.ts +0 -1385
  257. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  258. 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
- }