@visual-z/xeno 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +143 -33
- package/docs/install.md +18 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +42 -14
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup.ts +400 -268
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/progress.ts +9 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/store/workspace.ts +6 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
package/src/tui/transcript.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { Container, PiText, Lines, assistantMarkdown } from "./host.ts";
|
|
1
|
+
import { Container, PiText, Lines, assistantMarkdown, type MarkdownFeatures } from "./host.ts";
|
|
2
|
+
import { Image } from "../vendor/pi-tui/components/image.ts";
|
|
2
3
|
import { renderToolView } from "./toolview.ts";
|
|
3
4
|
import { PLAIN_ATTRS, paint, type Style } from "./style.ts";
|
|
4
5
|
import { applyBackgroundToLine } from "../vendor/pi-tui/utils.ts";
|
|
6
|
+
import type { Component } from "../vendor/pi-tui/tui.ts";
|
|
5
7
|
|
|
6
8
|
/** Presentation DTO; the application supplies its pure log projection. */
|
|
9
|
+
type Attachment = { sha256: string; name: string; width: number; height: number };
|
|
7
10
|
type Item = { id: string; revision: string } & (
|
|
8
|
-
| { kind: "user"; text: string; interrupted: boolean }
|
|
11
|
+
| { kind: "user"; text: string; interrupted: boolean; images?: readonly Attachment[] }
|
|
9
12
|
| { kind: "assistant" | "thinking" | "notice"; text: string }
|
|
10
|
-
| { kind: "
|
|
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 }
|
|
11
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
|
+
}
|
|
12
25
|
|
|
13
26
|
export class Transcript extends Container {
|
|
14
27
|
private items: readonly Item[] = [];
|
|
@@ -16,11 +29,29 @@ export class Transcript extends Container {
|
|
|
16
29
|
private style: Style | null = null;
|
|
17
30
|
private toolsExpanded = false;
|
|
18
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
|
+
}
|
|
19
50
|
|
|
20
51
|
update(items: readonly Item[], style: Style, toolsExpanded: boolean, thinkingExpanded: boolean): void {
|
|
21
52
|
const changed = style !== this.style || toolsExpanded !== this.toolsExpanded || thinkingExpanded !== this.thinkingExpanded;
|
|
22
53
|
if (!changed && this.items === items) return;
|
|
23
|
-
if (changed) this.
|
|
54
|
+
if (changed) this.clearCache();
|
|
24
55
|
this.items = items;
|
|
25
56
|
this.style = style;
|
|
26
57
|
this.toolsExpanded = toolsExpanded;
|
|
@@ -33,6 +64,7 @@ export class Transcript extends Container {
|
|
|
33
64
|
next.set(item.id, { revision: item.revision, component });
|
|
34
65
|
this.addChild(component);
|
|
35
66
|
}
|
|
67
|
+
for (const [id, entry] of this.cache) if (next.get(id)?.component !== entry.component) dispose(entry.component);
|
|
36
68
|
this.cache = next;
|
|
37
69
|
}
|
|
38
70
|
|
|
@@ -40,15 +72,20 @@ export class Transcript extends Container {
|
|
|
40
72
|
const component = new Container();
|
|
41
73
|
const dim = (text: string) => paint(text, { ...PLAIN_ATTRS, dim: true }, style);
|
|
42
74
|
switch (item.kind) {
|
|
43
|
-
case "user":
|
|
44
|
-
component.addChild(new
|
|
45
|
-
(text
|
|
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);
|
|
46
82
|
if (item.interrupted) component.addChild(new PiText(dim(" interrupted"), 0, 0));
|
|
47
83
|
break;
|
|
48
|
-
|
|
84
|
+
}
|
|
85
|
+
case "assistant": component.addChild(assistantMarkdown(item.text, style, this.features)); break;
|
|
49
86
|
case "thinking": {
|
|
50
87
|
const expanded = this.thinkingExpanded;
|
|
51
|
-
component.addChild(new Lines((width) => renderToolView({ kind: "thinking", text: item.text }, expanded, width, style)));
|
|
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)));
|
|
52
89
|
break;
|
|
53
90
|
}
|
|
54
91
|
case "tool": {
|
|
@@ -60,12 +97,27 @@ export class Transcript extends Container {
|
|
|
60
97
|
const result = item.result;
|
|
61
98
|
rows.push(new Lines((width) => renderToolView({ kind: "tool_result", tool, text: result.text, ok: result.ok }, expanded, width, style)));
|
|
62
99
|
} else rows.push(new Lines(() => [dim(" pending")]));
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
65
105
|
break;
|
|
66
106
|
}
|
|
67
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
|
+
}
|
|
68
112
|
}
|
|
69
113
|
return component;
|
|
70
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
|
+
}
|
|
71
123
|
}
|
package/src/tui/tree.ts
CHANGED
|
@@ -15,8 +15,9 @@ import type { Component } from "./text.ts";
|
|
|
15
15
|
import { displayWidth } from "./width.ts";
|
|
16
16
|
import { PLAIN_ATTRS, paint, type Style } from "./style.ts";
|
|
17
17
|
import { Input } from "../vendor/pi-tui/components/input.ts";
|
|
18
|
-
import { encodeKey
|
|
18
|
+
import { encodeKey } from "./input.ts";
|
|
19
19
|
import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
|
|
20
|
+
import { getKeybindings } from "../vendor/pi-tui/keybindings.ts";
|
|
20
21
|
|
|
21
22
|
/** Same mark /back uses for a branch the session rewound away from. */
|
|
22
23
|
export const LEFT_BRANCH_MARK = "↩";
|
|
@@ -42,7 +43,7 @@ const ON_PATH_MARK = " ";
|
|
|
42
43
|
const LEFT_MARK = `${LEFT_BRANCH_MARK} `;
|
|
43
44
|
|
|
44
45
|
export type TreeKind = "user" | "other";
|
|
45
|
-
export type TreeFilter = "user" | "all";
|
|
46
|
+
export type TreeFilter = "user" | "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
46
47
|
|
|
47
48
|
export interface TreeNode {
|
|
48
49
|
readonly id: string;
|
|
@@ -51,7 +52,12 @@ export interface TreeNode {
|
|
|
51
52
|
readonly current: boolean;
|
|
52
53
|
readonly kind: TreeKind;
|
|
53
54
|
readonly children: readonly TreeNode[];
|
|
54
|
-
|
|
55
|
+
readonly category?: "tool" | "message" | "metadata";
|
|
56
|
+
readonly label?: string;
|
|
57
|
+
readonly labelAt?: string;
|
|
58
|
+
readonly at?: string;
|
|
59
|
+
readonly content?: string;
|
|
60
|
+
/** Exact event position; user messages navigate before themselves. */
|
|
55
61
|
readonly target?: string;
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -71,6 +77,7 @@ export interface TreeState {
|
|
|
71
77
|
export interface CreateTreeOptions {
|
|
72
78
|
readonly filter?: TreeFilter;
|
|
73
79
|
readonly style?: Style;
|
|
80
|
+
readonly bindings?: Readonly<Record<string, readonly string[]>>;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
/**
|
|
@@ -82,6 +89,13 @@ export interface TreePoint {
|
|
|
82
89
|
readonly atEvent: string;
|
|
83
90
|
readonly prompt: string;
|
|
84
91
|
readonly onPath: boolean;
|
|
92
|
+
readonly kind?: TreeKind;
|
|
93
|
+
readonly category?: "tool" | "message" | "metadata";
|
|
94
|
+
readonly label?: string;
|
|
95
|
+
readonly labelAt?: string;
|
|
96
|
+
readonly at?: string;
|
|
97
|
+
readonly content?: string;
|
|
98
|
+
readonly target?: string;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
export type NodeParent = { readonly node: number; readonly at_event?: string } | null;
|
|
@@ -131,19 +145,25 @@ export function applyTreeKey(state: TreeState, key: Key): TreeState {
|
|
|
131
145
|
function visibleNodes(nodes: readonly TreeNode[], filter: TreeFilter): TreeNode[] {
|
|
132
146
|
if (filter === "all") return [...nodes];
|
|
133
147
|
const out: TreeNode[] = [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
const stack = [...nodes].reverse().map(node => ({ node, into: out }));
|
|
149
|
+
while (stack.length) {
|
|
150
|
+
const { node: n, into } = stack.pop()!;
|
|
151
|
+
const include = filter === "user" || filter === "user-only" ? n.kind === "user" :
|
|
152
|
+
filter === "labeled-only" ? Boolean(n.label) : filter === "no-tools" ? n.category !== "tool" && n.category !== "metadata" : n.category !== "metadata";
|
|
153
|
+
const children: TreeNode[] = [];
|
|
154
|
+
if (include) into.push({ ...n, children });
|
|
155
|
+
for (let i = n.children.length - 1; i >= 0; i--) stack.push({ node: n.children[i]!, into: include ? children : into });
|
|
138
156
|
}
|
|
139
157
|
return out;
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
function flatten(nodes: readonly TreeNode[]): TreeNode[] {
|
|
143
161
|
const out: TreeNode[] = [];
|
|
144
|
-
|
|
162
|
+
const stack = [...nodes].reverse();
|
|
163
|
+
while (stack.length) {
|
|
164
|
+
const n = stack.pop()!;
|
|
145
165
|
out.push(n);
|
|
146
|
-
|
|
166
|
+
for (let i = n.children.length - 1; i >= 0; i--) stack.push(n.children[i]!);
|
|
147
167
|
}
|
|
148
168
|
return out;
|
|
149
169
|
}
|
|
@@ -162,7 +182,7 @@ function confirm(state: TreeState): TreeState {
|
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
function toggleFilter(state: TreeState): TreeState {
|
|
165
|
-
const filter: TreeFilter = state.filter === "all" ? "user" : "all";
|
|
185
|
+
const filter: TreeFilter = state.filter === "all" || state.filter === "default" ? "user" : "all";
|
|
166
186
|
const n = flatten(visibleNodes(state.nodes, filter)).length;
|
|
167
187
|
const selected = n === 0 ? 0 : Math.min(state.selected, n - 1);
|
|
168
188
|
return { ...state, filter, selected, confirmed: null, cancelled: false };
|
|
@@ -219,15 +239,14 @@ function walk(
|
|
|
219
239
|
style: Style | undefined,
|
|
220
240
|
lines: string[],
|
|
221
241
|
): void {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
const branch = isRoot ? "" : last ? BRANCH_END : BRANCH_MID;
|
|
242
|
+
const stack = nodes.map((node, i) => ({ node, prefix, root: isRoot, last: i === nodes.length - 1 })).reverse();
|
|
243
|
+
while (stack.length) {
|
|
244
|
+
const { node, prefix: indent, root, last } = stack.pop()!;
|
|
245
|
+
const branch = root ? "" : last ? BRANCH_END : BRANCH_MID;
|
|
227
246
|
const sel = node.id === selectedId ? SELECTED : UNSELECTED;
|
|
228
247
|
const mark = node.current ? CURRENT_MARK : node.onPath ? ON_PATH_MARK : LEFT_MARK;
|
|
229
248
|
// Left-to-right: connectors stay, prompt is what width drops first.
|
|
230
|
-
const raw = sel +
|
|
249
|
+
const raw = sel + indent + branch + mark + (node.label ? `[${node.label}] ` : "") + node.prompt;
|
|
231
250
|
const clipped = truncate(raw, width);
|
|
232
251
|
const chosen = node.id === selectedId;
|
|
233
252
|
lines.push(
|
|
@@ -235,8 +254,11 @@ function walk(
|
|
|
235
254
|
? paint(clipped, { ...PLAIN_ATTRS, selected: true }, style)
|
|
236
255
|
: clipped,
|
|
237
256
|
);
|
|
238
|
-
const
|
|
239
|
-
|
|
257
|
+
const nextPrefix = root ? "" : indent + (last ? BLANK : PIPE);
|
|
258
|
+
// Bound indentation to the display, independently of history depth.
|
|
259
|
+
const budget = Math.max(3, Math.min(30, width - 20));
|
|
260
|
+
const childPrefix = nextPrefix.length > budget ? "… " + nextPrefix.slice(-(budget - 2)) : nextPrefix;
|
|
261
|
+
for (let i = node.children.length - 1; i >= 0; i--) stack.push({ node: node.children[i]!, prefix: childPrefix, root: false, last: i === node.children.length - 1 });
|
|
240
262
|
}
|
|
241
263
|
}
|
|
242
264
|
|
|
@@ -259,129 +281,168 @@ export function treeFromPoints(
|
|
|
259
281
|
byNode.set(p.node, list);
|
|
260
282
|
}
|
|
261
283
|
|
|
262
|
-
const childrenOf = new Map<number, number[]>();
|
|
263
|
-
const nodeIds = new Set<number>([...parents.keys(), ...byNode.keys()]);
|
|
264
|
-
for (const id of nodeIds) {
|
|
265
|
-
const parent = parents.get(id);
|
|
266
|
-
if (parent === undefined || parent === null) continue;
|
|
267
|
-
const list = childrenOf.get(parent.node) ?? [];
|
|
268
|
-
if (!list.includes(id)) list.push(id);
|
|
269
|
-
childrenOf.set(parent.node, list);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
284
|
const lastHere = [...points].reverse().find((p) => p.onPath && p.node === currentNode);
|
|
273
285
|
const lastOnPath = lastHere ?? [...points].reverse().find((p) => p.onPath);
|
|
274
286
|
|
|
275
287
|
function toNode(p: TreePoint, children: readonly TreeNode[]): TreeNode {
|
|
276
288
|
return {
|
|
289
|
+
...p,
|
|
277
290
|
id: `${p.node}:${p.atEvent}`,
|
|
278
291
|
prompt: p.prompt,
|
|
279
292
|
onPath: p.onPath,
|
|
280
293
|
current: lastOnPath !== undefined && lastOnPath.node === p.node && lastOnPath.atEvent === p.atEvent,
|
|
281
|
-
kind: "user",
|
|
294
|
+
kind: p.kind ?? "user",
|
|
282
295
|
children,
|
|
283
296
|
};
|
|
284
297
|
}
|
|
285
298
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
299
|
+
// Wire event chains and branch edges separately. Recursing once per event
|
|
300
|
+
// overflows the JS stack on otherwise valid long sessions.
|
|
301
|
+
type MutableNode = TreeNode & { children: TreeNode[] };
|
|
302
|
+
const chains = new Map<number, MutableNode[]>();
|
|
303
|
+
const events = new Map<string, MutableNode>();
|
|
304
|
+
for (const [id, pts] of byNode) {
|
|
305
|
+
const nodes = pts.map(p => toNode(p, []) as MutableNode);
|
|
306
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
307
|
+
events.set(nodes[i]!.id, nodes[i]!);
|
|
308
|
+
if (nodes[i + 1]) nodes[i]!.children.push(nodes[i + 1]!);
|
|
297
309
|
}
|
|
298
|
-
|
|
310
|
+
chains.set(id, nodes);
|
|
299
311
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
out.push(...chain(childId));
|
|
309
|
-
}
|
|
310
|
-
return out;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function chain(nodeId: number): TreeNode[] {
|
|
314
|
-
const pts = byNode.get(nodeId) ?? [];
|
|
315
|
-
if (pts.length === 0) return danglingForks(nodeId);
|
|
316
|
-
|
|
317
|
-
function fromIndex(i: number): TreeNode[] {
|
|
318
|
-
const p = pts[i];
|
|
319
|
-
if (p === undefined) return danglingForks(nodeId);
|
|
320
|
-
const forks = forksAt(nodeId, p.atEvent);
|
|
321
|
-
// Continuation (next point in this node) first, then forks at this event.
|
|
322
|
-
const children = i + 1 < pts.length
|
|
323
|
-
? [...fromIndex(i + 1), ...forks]
|
|
324
|
-
: [...forks, ...danglingForks(nodeId)];
|
|
325
|
-
return [toNode(p, children)];
|
|
312
|
+
const forest: TreeNode[] = [];
|
|
313
|
+
for (const [id, nodes] of [...chains].sort(([a], [b]) => a - b)) {
|
|
314
|
+
const first = nodes[0];
|
|
315
|
+
if (!first) continue;
|
|
316
|
+
let parent = parents.get(id);
|
|
317
|
+
const visited = new Set<number>([id]);
|
|
318
|
+
while (parent && !chains.get(parent.node)?.length && !visited.has(parent.node)) {
|
|
319
|
+
visited.add(parent.node); parent = parents.get(parent.node);
|
|
326
320
|
}
|
|
327
|
-
|
|
328
|
-
|
|
321
|
+
const anchor = parent && !visited.has(parent.node) ? events.get(`${parent.node}:${parent.at_event}`) ?? chains.get(parent.node)?.at(-1) : undefined;
|
|
322
|
+
if (anchor) anchor.children.push(first); else forest.push(first);
|
|
329
323
|
}
|
|
330
|
-
|
|
331
|
-
const roots: number[] = [];
|
|
332
|
-
for (const id of nodeIds) {
|
|
333
|
-
const parent = parents.get(id);
|
|
334
|
-
if (parent === undefined || parent === null) roots.push(id);
|
|
335
|
-
}
|
|
336
|
-
roots.sort((a, b) => a - b);
|
|
337
|
-
|
|
338
|
-
const forest: TreeNode[] = [];
|
|
339
|
-
for (const r of roots) forest.push(...chain(r));
|
|
340
324
|
return forest;
|
|
341
325
|
}
|
|
342
326
|
|
|
343
327
|
export class TreeList implements Component {
|
|
344
328
|
private current: TreeState;
|
|
345
329
|
private style: Style | undefined;
|
|
330
|
+
private readonly bindings: Readonly<Record<string, readonly string[]>> | undefined;
|
|
346
331
|
private readonly search = new Input({ prompt: "Search tree: " });
|
|
332
|
+
private readonly folded = new Set<string>();
|
|
333
|
+
private labelEditor: { node: TreeNode; input: Input } | undefined;
|
|
334
|
+
labelChange: { target: string; text: string } | undefined;
|
|
335
|
+
copyText: string | undefined;
|
|
336
|
+
showTimestamps = false;
|
|
347
337
|
onInput: ((data: string) => void) | undefined;
|
|
348
338
|
|
|
349
339
|
constructor(nodes: readonly TreeNode[], opts: CreateTreeOptions = {}) {
|
|
350
340
|
this.current = createTree(nodes, opts);
|
|
351
341
|
this.style = opts.style;
|
|
342
|
+
this.bindings = opts.bindings;
|
|
352
343
|
}
|
|
353
344
|
|
|
354
345
|
get state(): TreeState {
|
|
355
346
|
return { ...this.current, nodes: this.filtered() };
|
|
356
347
|
}
|
|
348
|
+
get selectedNode(): TreeNode | undefined { return flatten(this.filtered())[this.current.selected]; }
|
|
349
|
+
setNodes(nodes: readonly TreeNode[]): void {
|
|
350
|
+
const id = this.selectedNode?.id;
|
|
351
|
+
this.current = { ...this.current, nodes, selected: 0 };
|
|
352
|
+
this.current = { ...this.current, selected: Math.max(0, flatten(this.filtered()).findIndex(node => node.id === id)) };
|
|
353
|
+
}
|
|
357
354
|
|
|
358
355
|
applyKey(key: Key): TreeState {
|
|
359
356
|
return this.acceptInput(encodeKey(key));
|
|
360
357
|
}
|
|
361
358
|
|
|
362
|
-
get focused(): boolean { return this.search.focused; }
|
|
363
|
-
set focused(value: boolean) { this.search.focused = value; }
|
|
359
|
+
get focused(): boolean { return (this.labelEditor?.input ?? this.search).focused; }
|
|
360
|
+
set focused(value: boolean) { (this.labelEditor?.input ?? this.search).focused = value; }
|
|
364
361
|
setStyle(style: Style): void { this.style = style; }
|
|
365
362
|
invalidate(): void { this.search.invalidate(); }
|
|
366
363
|
handleInput(data: string): void { if (this.onInput) this.onInput(data); else this.acceptInput(data); }
|
|
367
364
|
private filtered(): readonly TreeNode[] {
|
|
368
|
-
const
|
|
365
|
+
const tokens = this.search.getValue().toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
369
366
|
const nodes = visibleNodes(this.current.nodes, this.current.filter);
|
|
370
|
-
if (
|
|
371
|
-
|
|
367
|
+
if (tokens.length) return flatten(nodes).filter(node => tokens.every(token => `${node.content ?? node.prompt} ${node.label ?? ""}`.toLowerCase().includes(token))).map(node => ({ ...node, children: [] }));
|
|
368
|
+
const out: TreeNode[] = [];
|
|
369
|
+
const stack = [...nodes].reverse().map(node => ({ node, into: out }));
|
|
370
|
+
while (stack.length) {
|
|
371
|
+
const { node, into } = stack.pop()!;
|
|
372
|
+
const children: TreeNode[] = [];
|
|
373
|
+
into.push({ ...node, prompt: `${this.showTimestamps && node.labelAt ? `${node.labelAt} ` : ""}${this.folded.has(node.id) ? "▸ " : ""}${node.prompt}`, children });
|
|
374
|
+
if (!this.folded.has(node.id)) for (let i = node.children.length - 1; i >= 0; i--) stack.push({ node: node.children[i]!, into: children });
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
372
377
|
}
|
|
373
|
-
acceptInput(data: string): TreeState {
|
|
378
|
+
acceptInput(data: string, action?: string): TreeState {
|
|
374
379
|
this.current = { ...this.current, confirmed: null, cancelled: false };
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
380
|
+
this.labelChange = undefined; this.copyText = undefined;
|
|
381
|
+
const kb = getKeybindings();
|
|
382
|
+
if (this.labelEditor) {
|
|
383
|
+
if (kb.matches(data, "tui.select.cancel")) this.labelEditor = undefined;
|
|
384
|
+
else if (kb.matches(data, "tui.select.confirm")) {
|
|
385
|
+
this.labelChange = { target: this.labelEditor.node.target ?? this.labelEditor.node.id, text: this.labelEditor.input.getValue() };
|
|
386
|
+
this.labelEditor = undefined;
|
|
387
|
+
} else this.labelEditor.input.handleInput(data);
|
|
388
|
+
return this.state;
|
|
389
|
+
}
|
|
390
|
+
const filters: TreeFilter[] = ["default", "no-tools", "user-only", "labeled-only", "all"];
|
|
391
|
+
const filterByAction: Record<string, TreeFilter> = { "app.tree.filter.default": "default", "app.tree.filter.noTools": "no-tools",
|
|
392
|
+
"app.tree.filter.userOnly": "user-only", "app.tree.filter.labeledOnly": "labeled-only", "app.tree.filter.all": "all" };
|
|
393
|
+
const filter = action ? filterByAction[action] : undefined;
|
|
394
|
+
if (filter) { this.folded.clear(); this.current = { ...this.current, filter: this.current.filter === filter ? "default" : filter, selected: 0 }; return this.state; }
|
|
395
|
+
if (action === "app.tree.filter.cycleForward" || action === "app.tree.filter.cycleBackward") {
|
|
396
|
+
const index = Math.max(0, filters.indexOf(this.current.filter));
|
|
397
|
+
this.folded.clear();
|
|
398
|
+
this.current = { ...this.current, filter: filters[(index + (action.endsWith("Forward") ? 1 : 4)) % 5]!, selected: 0 }; return this.state;
|
|
399
|
+
}
|
|
400
|
+
if (action === "app.tree.toggleLabelTimestamp") { this.showTimestamps = !this.showTimestamps; return this.state; }
|
|
401
|
+
const selected = this.selectedNode;
|
|
402
|
+
if (action === "app.tree.editLabel") {
|
|
403
|
+
if (selected) { const input = new Input({ prompt: "Label: " }); input.setValue(selected.label ?? ""); input.focused = this.focused; this.labelEditor = { node: selected, input }; }
|
|
404
|
+
return this.state;
|
|
405
|
+
}
|
|
406
|
+
if (action === "app.message.copy") { this.copyText = selected?.content ?? selected?.prompt; return this.state; }
|
|
407
|
+
if (selected && (action === "app.tree.foldOrUp" || action === "app.tree.unfoldOrDown")) {
|
|
408
|
+
const flat = flatten(this.filtered());
|
|
409
|
+
const parentOf = new Map<string, TreeNode>();
|
|
410
|
+
for (const node of flat) for (const child of node.children) parentOf.set(child.id, node);
|
|
411
|
+
const parent = parentOf.get(selected.id);
|
|
412
|
+
if (action === "app.tree.foldOrUp") {
|
|
413
|
+
if (selected.children.length && (!parent || parent.children.length > 1) && !this.folded.has(selected.id)) this.folded.add(selected.id);
|
|
414
|
+
else {
|
|
415
|
+
let target = selected;
|
|
416
|
+
while (parentOf.has(target.id)) {
|
|
417
|
+
const ancestor = parentOf.get(target.id)!;
|
|
418
|
+
if (ancestor.children.length > 1 && target.id !== selected.id) break;
|
|
419
|
+
target = ancestor;
|
|
420
|
+
}
|
|
421
|
+
this.current = { ...this.current, selected: flat.findIndex(node => node.id === target.id) };
|
|
422
|
+
}
|
|
423
|
+
} else if (this.folded.delete(selected.id)) { /* Expand in place before moving to a child. */ }
|
|
424
|
+
else {
|
|
425
|
+
let target = selected;
|
|
426
|
+
while (target.children.length) { const branch = target.children.length > 1; target = target.children[0]!; if (branch) break; }
|
|
427
|
+
this.current = { ...this.current, selected: flat.findIndex(node => node.id === target.id) };
|
|
428
|
+
}
|
|
429
|
+
return this.state;
|
|
430
|
+
}
|
|
431
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
432
|
+
if (this.search.getValue()) { this.search.setValue(""); this.folded.clear(); this.current = { ...this.current, selected: 0 }; }
|
|
433
|
+
else this.current = { ...this.current, cancelled: true };
|
|
434
|
+
}
|
|
435
|
+
else if (kb.matches(data, "tui.select.confirm")) this.current = { ...this.current, confirmed: selected ?? null };
|
|
436
|
+
else if (kb.matches(data, "tui.select.up") || kb.matches(data, "tui.select.down")) {
|
|
437
|
+
const size = flatten(this.filtered()).length;
|
|
438
|
+
this.current = { ...this.current, selected: size ? (this.current.selected + (kb.matches(data, "tui.select.up") ? -1 : 1) + size) % size : 0 };
|
|
439
|
+
} else if (kb.matches(data, "tui.input.tab")) this.current = toggleFilter(this.current);
|
|
440
|
+
else if (kb.matches(data, "tui.select.pageUp") || kb.matches(data, "tui.select.pageDown") || kb.matches(data, "tui.editor.cursorLeft") || kb.matches(data, "tui.editor.cursorRight")) {
|
|
381
441
|
this.current = { ...this.current, selected: Math.max(0, Math.min(flatten(this.filtered()).length - 1,
|
|
382
|
-
this.current.selected + (
|
|
442
|
+
this.current.selected + (kb.matches(data, "tui.select.pageUp") || kb.matches(data, "tui.editor.cursorLeft") ? -10 : 10))) };
|
|
383
443
|
} else {
|
|
384
|
-
this.search.handleInput(data);
|
|
444
|
+
const before = this.search.getValue(); this.search.handleInput(data);
|
|
445
|
+
if (before !== this.search.getValue()) this.folded.clear();
|
|
385
446
|
this.current = { ...this.current, selected: 0 };
|
|
386
447
|
}
|
|
387
448
|
return this.state;
|
|
@@ -389,6 +450,8 @@ export class TreeList implements Component {
|
|
|
389
450
|
|
|
390
451
|
render(width: number): string[] {
|
|
391
452
|
if (width <= 0) return [""];
|
|
453
|
+
if (this.labelEditor) return ["Edit label · empty removes it", ...this.labelEditor.input.render(width),
|
|
454
|
+
`${getKeybindings().getKeys("tui.select.confirm").join("/")} save · ${getKeybindings().getKeys("tui.select.cancel").join("/")} cancel`].map(line => truncateToWidth(line, width, ""));
|
|
392
455
|
const body = renderTree(
|
|
393
456
|
this.filtered(),
|
|
394
457
|
{ index: this.current.selected, filter: this.current.filter },
|
|
@@ -398,8 +461,11 @@ export class TreeList implements Component {
|
|
|
398
461
|
// This is a viewport, not a total result cap. Arrows/search can reach every row.
|
|
399
462
|
const start = Math.max(0, this.current.selected - 5);
|
|
400
463
|
const rows = body.slice(start, start + 10);
|
|
401
|
-
|
|
402
|
-
|
|
464
|
+
const hints = this.bindings ? [["app.tree.filter.cycleForward", "filters"], ["app.tree.foldOrUp", "branch"], ["app.tree.editLabel", "label"],
|
|
465
|
+
["app.tree.toggleLabelTimestamp", "label time"], ["app.message.copy", "copy"], ["tui.select.cancel", "cancel"]]
|
|
466
|
+
.flatMap(([action, label]) => this.bindings![action!]?.length ? [`${this.bindings![action!]!.join("/")} ${label}`] : []).join(" · ") : TREE_HINT;
|
|
467
|
+
return ["Session tree · Select an event to navigate", ...this.search.render(width),
|
|
468
|
+
...rows, `${this.current.filter} · ${hints}`]
|
|
403
469
|
.map((line) => truncateToWidth(line, width, ""));
|
|
404
470
|
}
|
|
405
471
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repository": "https://github.com/earendil-works/pi",
|
|
3
|
+
"commit": "02eed88fd8912e54a804ddebd409e2e4c08ac5ef",
|
|
4
|
+
"path": "packages/coding-agent/src/utils",
|
|
5
|
+
"files": {
|
|
6
|
+
"syntax-highlight.ts": "626f94ab2eae32d77dd473bc24fa2d170fbf07d1",
|
|
7
|
+
"html.ts": "a13ad46f4888ad62587de59b0670b641a1e2f0a8",
|
|
8
|
+
"mime.ts": "9016b790f09f1073a707a9a94574c4fd3ae1c0f2",
|
|
9
|
+
"photon.ts": "6c320705eb975969abe145277566e3297a483272",
|
|
10
|
+
"exif-orientation.ts": "d1c47df1b7f1a63837ebbb9adb9cedee308a35e8",
|
|
11
|
+
"image-resize-core.ts": "e60820c7baedaf89d8b387b208c811c468d74ae4",
|
|
12
|
+
"image-resize.ts": "516a1e57a6be3d6840cb5b0fced3be6b3bc30286",
|
|
13
|
+
"image-resize-worker.ts": "ee881b3d9794ae60c28d8f6ee4d575d810638c17",
|
|
14
|
+
"image-convert.ts": "f781d53d1b195b1a8ced2822263f840a06350007",
|
|
15
|
+
"clipboard-command.ts": "700fc8e609478f76dc614331b5281d0f219f3641"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** Undefined means the command failed; an empty buffer is a successful result. */
|
|
4
|
+
export function runClipboardCommand(
|
|
5
|
+
command: string,
|
|
6
|
+
args: readonly string[],
|
|
7
|
+
options?: { input?: string; timeoutMs?: number; maxBufferBytes?: number },
|
|
8
|
+
): Promise<Buffer | undefined> {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
// Clipboard writers can daemonize. Do not give them output pipes to retain.
|
|
11
|
+
const child = spawn(command, args, {
|
|
12
|
+
stdio: ["pipe", options?.input === undefined ? "pipe" : "ignore", "ignore"],
|
|
13
|
+
windowsHide: true,
|
|
14
|
+
});
|
|
15
|
+
const chunks: Buffer[] = [];
|
|
16
|
+
let length = 0;
|
|
17
|
+
let settled = false;
|
|
18
|
+
const finish = (result: Buffer | undefined): void => {
|
|
19
|
+
if (settled) return;
|
|
20
|
+
settled = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolve(result);
|
|
23
|
+
};
|
|
24
|
+
const abort = (): void => {
|
|
25
|
+
child.kill("SIGKILL");
|
|
26
|
+
child.stdout?.destroy();
|
|
27
|
+
child.stdin?.destroy();
|
|
28
|
+
finish(undefined);
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(abort, options?.timeoutMs ?? 3000);
|
|
31
|
+
child.on("error", () => finish(undefined));
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (!settled) finish(code === 0 ? Buffer.concat(chunks, length) : undefined);
|
|
34
|
+
});
|
|
35
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
length += chunk.length;
|
|
38
|
+
if (length > (options?.maxBufferBytes ?? 50 * 1024 * 1024)) abort();
|
|
39
|
+
else chunks.push(chunk);
|
|
40
|
+
});
|
|
41
|
+
child.stdin?.on("error", () => {}); // A writer may exit before consuming all input.
|
|
42
|
+
child.stdin?.end(options?.input);
|
|
43
|
+
});
|
|
44
|
+
}
|