@visual-z/xeno 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- package/src/vendor/pi-tui/word-navigation.ts +117 -0
package/src/tui/text.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Container, PiText, Lines, assistantMarkdown } from "./host.ts";
|
|
2
|
+
import { renderToolView } from "./toolview.ts";
|
|
3
|
+
import { PLAIN_ATTRS, paint, type Style } from "./style.ts";
|
|
4
|
+
import { applyBackgroundToLine } from "../vendor/pi-tui/utils.ts";
|
|
5
|
+
|
|
6
|
+
/** Presentation DTO; the application supplies its pure log projection. */
|
|
7
|
+
type Item = { id: string; revision: string } & (
|
|
8
|
+
| { kind: "user"; text: string; interrupted: boolean }
|
|
9
|
+
| { kind: "assistant" | "thinking" | "notice"; text: string }
|
|
10
|
+
| { kind: "tool"; name: string; input: unknown; result: { text: string; ok: boolean } | null }
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export class Transcript extends Container {
|
|
14
|
+
private items: readonly Item[] = [];
|
|
15
|
+
private cache = new Map<string, { revision: string; component: Container }>();
|
|
16
|
+
private style: Style | null = null;
|
|
17
|
+
private toolsExpanded = false;
|
|
18
|
+
private thinkingExpanded = false;
|
|
19
|
+
|
|
20
|
+
update(items: readonly Item[], style: Style, toolsExpanded: boolean, thinkingExpanded: boolean): void {
|
|
21
|
+
const changed = style !== this.style || toolsExpanded !== this.toolsExpanded || thinkingExpanded !== this.thinkingExpanded;
|
|
22
|
+
if (!changed && this.items === items) return;
|
|
23
|
+
if (changed) this.cache.clear();
|
|
24
|
+
this.items = items;
|
|
25
|
+
this.style = style;
|
|
26
|
+
this.toolsExpanded = toolsExpanded;
|
|
27
|
+
this.thinkingExpanded = thinkingExpanded;
|
|
28
|
+
const next = new Map<string, { revision: string; component: Container }>();
|
|
29
|
+
this.clear();
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
const old = this.cache.get(item.id);
|
|
32
|
+
const component = old?.revision === item.revision ? old.component : this.component(item, style);
|
|
33
|
+
next.set(item.id, { revision: item.revision, component });
|
|
34
|
+
this.addChild(component);
|
|
35
|
+
}
|
|
36
|
+
this.cache = next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private component(item: Item, style: Style): Container {
|
|
40
|
+
const component = new Container();
|
|
41
|
+
const dim = (text: string) => paint(text, { ...PLAIN_ATTRS, dim: true }, style);
|
|
42
|
+
switch (item.kind) {
|
|
43
|
+
case "user":
|
|
44
|
+
component.addChild(new PiText(paint(`❯ ${item.text}`, { ...PLAIN_ATTRS, prompt: true }, style), 1, 1,
|
|
45
|
+
(text) => style.token?.("userMessageBg", text) ?? text));
|
|
46
|
+
if (item.interrupted) component.addChild(new PiText(dim(" interrupted"), 0, 0));
|
|
47
|
+
break;
|
|
48
|
+
case "assistant": component.addChild(assistantMarkdown(item.text, style)); break;
|
|
49
|
+
case "thinking": {
|
|
50
|
+
const expanded = this.thinkingExpanded;
|
|
51
|
+
component.addChild(new Lines((width) => renderToolView({ kind: "thinking", text: item.text }, expanded, width, style)));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "tool": {
|
|
55
|
+
const tool = { name: item.name, input: item.input };
|
|
56
|
+
const expanded = this.toolsExpanded;
|
|
57
|
+
const background = item.result === null ? "toolPendingBg" : item.result.ok ? "toolSuccessBg" : "toolErrorBg";
|
|
58
|
+
const rows: Lines[] = [new Lines((width) => renderToolView({ kind: "tool_call", tool }, expanded, width, style))];
|
|
59
|
+
if (item.result) {
|
|
60
|
+
const result = item.result;
|
|
61
|
+
rows.push(new Lines((width) => renderToolView({ kind: "tool_result", tool, text: result.text, ok: result.ok }, expanded, width, style)));
|
|
62
|
+
} else rows.push(new Lines(() => [dim(" pending")]));
|
|
63
|
+
component.addChild(new Lines((width) => rows.flatMap((row) => row.render(width)).map((line) =>
|
|
64
|
+
applyBackgroundToLine(line, width, (text) => style.token?.(background, text) ?? text))));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "notice": component.addChild(new PiText(dim(item.text), 0, 0)); break;
|
|
68
|
+
}
|
|
69
|
+
return component;
|
|
70
|
+
}
|
|
71
|
+
}
|