@forsion/tangu-computer-use 0.5.8

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 (120) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/LICENSE +26 -0
  3. package/LICENSE.upstream +21 -0
  4. package/README.md +169 -0
  5. package/UPSTREAM.md +277 -0
  6. package/check.mjs +369 -0
  7. package/icon.png +0 -0
  8. package/install.sh +112 -0
  9. package/main.js +541 -0
  10. package/manifest.json +47 -0
  11. package/native/linux/bridge-rs/Cargo.lock +1204 -0
  12. package/native/linux/bridge-rs/Cargo.toml +18 -0
  13. package/native/linux/bridge-rs/src/atspi.rs +640 -0
  14. package/native/linux/bridge-rs/src/error.rs +65 -0
  15. package/native/linux/bridge-rs/src/lib.rs +11 -0
  16. package/native/linux/bridge-rs/src/main.rs +1074 -0
  17. package/native/linux/bridge-rs/src/protocol.rs +49 -0
  18. package/native/linux/bridge-rs/src/state.rs +293 -0
  19. package/native/linux/bridge-rs/src/wayland.rs +89 -0
  20. package/native/linux/bridge-rs/src/x11.rs +909 -0
  21. package/native/linux/bridge-rs/tests/protocol_tests.rs +73 -0
  22. package/native/macos/agent_cursor.swift +184 -0
  23. package/native/macos/agent_cursor_motion.swift +252 -0
  24. package/native/macos/agent_cursor_tests.swift +128 -0
  25. package/native/macos/agent_highlight.swift +229 -0
  26. package/native/macos/agent_highlight_tests.swift +99 -0
  27. package/native/macos/bridge.swift +3852 -0
  28. package/native/macos/foreground_activity.swift +57 -0
  29. package/native/macos/foreground_activity_tests.swift +31 -0
  30. package/native/macos/live_stream.swift +289 -0
  31. package/native/windows/bridge-rs/Cargo.lock +396 -0
  32. package/native/windows/bridge-rs/Cargo.toml +30 -0
  33. package/native/windows/bridge-rs/src/capture.rs +518 -0
  34. package/native/windows/bridge-rs/src/error.rs +81 -0
  35. package/native/windows/bridge-rs/src/input.rs +501 -0
  36. package/native/windows/bridge-rs/src/lib.rs +16 -0
  37. package/native/windows/bridge-rs/src/main.rs +1683 -0
  38. package/native/windows/bridge-rs/src/protocol.rs +53 -0
  39. package/native/windows/bridge-rs/src/refs.rs +237 -0
  40. package/native/windows/bridge-rs/src/state.rs +55 -0
  41. package/native/windows/bridge-rs/src/uia.rs +1252 -0
  42. package/native/windows/bridge-rs/src/window.rs +718 -0
  43. package/native/windows/bridge-rs/tests/protocol_tests.rs +252 -0
  44. package/native/windows/bridge-rs/tests/refs_tests.rs +86 -0
  45. package/native/windows/bridge-rs/tests/state_tests.rs +33 -0
  46. package/package.json +74 -0
  47. package/prebuilt/linux/arm64/linux-bridge +0 -0
  48. package/prebuilt/linux/x64/linux-bridge +0 -0
  49. package/prebuilt/macos/arm64/bridge +0 -0
  50. package/prebuilt/macos/arm64/tangu-computer-use.app.json +10 -0
  51. package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
  52. package/prebuilt/macos/x64/bridge +0 -0
  53. package/prebuilt/macos/x64/tangu-computer-use.app.json +10 -0
  54. package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
  55. package/prebuilt/windows/windows-bridge.exe +0 -0
  56. package/scripts/blind-click.check.mjs +128 -0
  57. package/scripts/build-native.mjs +302 -0
  58. package/scripts/build.mjs +84 -0
  59. package/scripts/calc-fixture.mjs +78 -0
  60. package/scripts/cli-exit.check.mjs +49 -0
  61. package/scripts/helper-path.check.mjs +82 -0
  62. package/scripts/helper-refresh.check.mjs +62 -0
  63. package/scripts/helper-signal.check.mjs +59 -0
  64. package/scripts/highlight.check.mjs +32 -0
  65. package/scripts/keychain-free-install.check.mjs +140 -0
  66. package/scripts/live-view.check.mjs +124 -0
  67. package/scripts/macos-bundle.d.mts +1 -0
  68. package/scripts/macos-bundle.mjs +119 -0
  69. package/scripts/make-signing-cert.sh +57 -0
  70. package/scripts/mini-foreground.check.mjs +11 -0
  71. package/scripts/no-foreground.check.mjs +81 -0
  72. package/scripts/overlay-visible.check.mjs +221 -0
  73. package/scripts/package-macos-app.mjs +39 -0
  74. package/scripts/permissions.check.mjs +199 -0
  75. package/scripts/platform-contract.check.mjs +30 -0
  76. package/scripts/setup-helper.mjs +154 -0
  77. package/scripts/tangu-computer-use.entitlements +10 -0
  78. package/scripts/verify-macos-bundles.mjs +25 -0
  79. package/scripts/verify-package.mjs +74 -0
  80. package/skills/computer-use/SKILL.md +110 -0
  81. package/src/foregroundNote.ts +43 -0
  82. package/src/helperState.ts +31 -0
  83. package/src/index.ts +63 -0
  84. package/src/onboarding.ts +198 -0
  85. package/src/pi-compat.ts +49 -0
  86. package/src/settings.ts +11 -0
  87. package/src/setup.ts +97 -0
  88. package/src/tools.ts +289 -0
  89. package/src/vendor/actions.ts +130 -0
  90. package/src/vendor/bridge.ts +2405 -0
  91. package/src/vendor/cdp.ts +658 -0
  92. package/src/vendor/config.ts +113 -0
  93. package/src/vendor/contract.ts +104 -0
  94. package/src/vendor/note.ts +195 -0
  95. package/src/vendor/outline.ts +651 -0
  96. package/src/vendor/output.ts +134 -0
  97. package/src/vendor/permissions.ts +111 -0
  98. package/src/vendor/platform/architecture.ts +23 -0
  99. package/src/vendor/platform/coerce.ts +16 -0
  100. package/src/vendor/platform/index.ts +59 -0
  101. package/src/vendor/platform/linux/backend.ts +186 -0
  102. package/src/vendor/platform/linux/helper.ts +238 -0
  103. package/src/vendor/platform/macos/backend.ts +131 -0
  104. package/src/vendor/platform/macos/browser.ts +110 -0
  105. package/src/vendor/platform/macos/helper-path.d.mts +9 -0
  106. package/src/vendor/platform/macos/helper-path.mjs +34 -0
  107. package/src/vendor/platform/macos/helper.ts +291 -0
  108. package/src/vendor/platform/macos/permissions.ts +146 -0
  109. package/src/vendor/platform/types.ts +222 -0
  110. package/src/vendor/platform/windows/backend.ts +142 -0
  111. package/src/vendor/platform/windows/helper.ts +140 -0
  112. package/src/vendor/root-selection.ts +25 -0
  113. package/src/vendor/runtime.ts +129 -0
  114. package/src/vendor/state.ts +146 -0
  115. package/src/vendor/view.ts +147 -0
  116. package/tangu-plugins/computer-use/dist/foregroundNote.js +19 -0
  117. package/tangu-plugins/computer-use/dist/index.js +5672 -0
  118. package/tangu-plugins/computer-use/tangu-plugin.json +9 -0
  119. package/tsconfig.json +22 -0
  120. package/types/tangu-agent.d.ts +142 -0
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+
5
+ export interface ComputerUseConfig {
6
+ browser_use: boolean;
7
+ headless: boolean;
8
+ cursor_overlay: boolean;
9
+ managed_browser: "helium" | "chrome";
10
+ }
11
+
12
+ export interface ComputerUseConfigSource {
13
+ path: string;
14
+ exists: boolean;
15
+ values?: Partial<ComputerUseConfig>;
16
+ error?: string;
17
+ }
18
+
19
+ export interface LoadedComputerUseConfig {
20
+ config: ComputerUseConfig;
21
+ sources: ComputerUseConfigSource[];
22
+ env: Partial<ComputerUseConfig>;
23
+ }
24
+
25
+ const DEFAULT_CONFIG: ComputerUseConfig = {
26
+ browser_use: true,
27
+ headless: false,
28
+ cursor_overlay: true,
29
+ managed_browser: "chrome",
30
+ };
31
+
32
+ let activeConfig: ComputerUseConfig = { ...DEFAULT_CONFIG };
33
+ let activeLoadedConfig: LoadedComputerUseConfig = { config: activeConfig, sources: [], env: {} };
34
+
35
+ function parseBoolean(value: unknown): boolean | undefined {
36
+ if (typeof value === "boolean") return value;
37
+ if (typeof value === "number") return value === 1 ? true : value === 0 ? false : undefined;
38
+ if (typeof value !== "string") return undefined;
39
+ const normalized = value.trim().toLowerCase();
40
+ if (["1", "true", "yes", "on", "enabled"].includes(normalized)) return true;
41
+ if (["0", "false", "no", "off", "disabled"].includes(normalized)) return false;
42
+ return undefined;
43
+ }
44
+
45
+ function normalizePartial(raw: unknown): Partial<ComputerUseConfig> {
46
+ if (!raw || typeof raw !== "object") return {};
47
+ const source = (raw as any).computer_use && typeof (raw as any).computer_use === "object" ? (raw as any).computer_use : raw;
48
+ const out: Partial<ComputerUseConfig> = {};
49
+ const browserUse = parseBoolean((source as any).browser_use);
50
+ const headless = parseBoolean((source as any).headless);
51
+ const cursorOverlay = parseBoolean((source as any).cursor_overlay);
52
+ if (browserUse !== undefined) out.browser_use = browserUse;
53
+ if (headless !== undefined) out.headless = headless;
54
+ if (cursorOverlay !== undefined) out.cursor_overlay = cursorOverlay;
55
+ const managedBrowser = (source as any).managed_browser;
56
+ if (managedBrowser === "helium" || managedBrowser === "chrome") out.managed_browser = managedBrowser;
57
+ return out;
58
+ }
59
+
60
+ function readConfigFile(filePath: string): ComputerUseConfigSource {
61
+ if (!existsSync(filePath)) return { path: filePath, exists: false };
62
+ try {
63
+ const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
64
+ return { path: filePath, exists: true, values: normalizePartial(parsed) };
65
+ } catch (error) {
66
+ return { path: filePath, exists: true, error: error instanceof Error ? error.message : String(error) };
67
+ }
68
+ }
69
+
70
+ function readEnv(): Partial<ComputerUseConfig> {
71
+ const out: Partial<ComputerUseConfig> = {};
72
+ const browserUse = parseBoolean(process.env.PI_COMPUTER_USE_BROWSER_USE);
73
+ const headless = parseBoolean(process.env.PI_COMPUTER_USE_HEADLESS);
74
+ const cursorOverlay = parseBoolean(process.env.PI_COMPUTER_USE_CURSOR_OVERLAY);
75
+ if (browserUse !== undefined) out.browser_use = browserUse;
76
+ if (headless !== undefined) out.headless = headless;
77
+ if (cursorOverlay !== undefined) out.cursor_overlay = cursorOverlay;
78
+ const managedBrowser = process.env.PI_COMPUTER_USE_MANAGED_BROWSER;
79
+ if (managedBrowser === "helium" || managedBrowser === "chrome") out.managed_browser = managedBrowser;
80
+ return out;
81
+ }
82
+
83
+ export function loadComputerUseConfig(cwd: string): LoadedComputerUseConfig {
84
+ const sources = [
85
+ readConfigFile(path.join(getAgentDir(), "extensions", "tangu-computer-use.json")),
86
+ readConfigFile(path.join(cwd, ".pi", "computer-use.json")),
87
+ ];
88
+ const env = readEnv();
89
+ const config = { ...DEFAULT_CONFIG };
90
+ for (const source of sources) {
91
+ if (source.values) Object.assign(config, source.values);
92
+ }
93
+ Object.assign(config, env);
94
+ activeConfig = config;
95
+ activeLoadedConfig = { config, sources, env };
96
+ return activeLoadedConfig;
97
+ }
98
+
99
+ export function getComputerUseConfig(): ComputerUseConfig {
100
+ return activeConfig;
101
+ }
102
+
103
+ export function getLoadedComputerUseConfig(): LoadedComputerUseConfig {
104
+ return activeLoadedConfig;
105
+ }
106
+
107
+ export function isHeadlessMode(): boolean {
108
+ return activeConfig.headless;
109
+ }
110
+
111
+ export function isBrowserUseEnabled(): boolean {
112
+ return activeConfig.browser_use;
113
+ }
@@ -0,0 +1,104 @@
1
+ export type RootSelector = string;
2
+ export type ImageMode = "auto" | "always" | "never";
3
+ export type MouseButtonName = "left" | "right" | "middle";
4
+
5
+ export interface ObserveTargetParams {
6
+ root?: RootSelector;
7
+ }
8
+
9
+ export interface FindParams {
10
+ text?: string;
11
+ app?: string;
12
+ bundleId?: string;
13
+ pid?: number;
14
+ /** Filters on the platform's best-effort presentation hint; only window vs transient is guaranteed. */
15
+ kind?: "window" | "menu" | "sheet" | "popover" | "dialog" | "browser_page";
16
+ }
17
+
18
+ export interface StateTargetParams {
19
+ stateId?: string;
20
+ }
21
+
22
+ export interface NavigateBrowserParams extends StateTargetParams {
23
+ url: string;
24
+ }
25
+
26
+ export interface LaunchBrowserParams {
27
+ url?: string;
28
+ }
29
+
30
+ export interface EvaluateBrowserParams {
31
+ stateId: string;
32
+ expression: string;
33
+ }
34
+
35
+ export interface ObserveParams extends ObserveTargetParams {
36
+ mode?: "semantic" | "visual" | "fused";
37
+ /** Internal capture override; not part of the model-facing schema. */
38
+ readText?: "auto" | "always" | "never";
39
+ }
40
+
41
+ export interface SearchUiParams extends StateTargetParams {
42
+ text?: string;
43
+ role?: string;
44
+ capability?: string;
45
+ }
46
+
47
+ export interface ExpandUiParams extends StateTargetParams {
48
+ ref: string;
49
+ depth?: number;
50
+ }
51
+
52
+ export interface InspectUiParams extends StateTargetParams {
53
+ ref: string;
54
+ }
55
+
56
+ export interface UiCondition {
57
+ ref?: string;
58
+ scopeRef?: string;
59
+ text?: string;
60
+ role?: string;
61
+ value?: string;
62
+ until?: "present" | "absent";
63
+ timeoutMs?: number;
64
+ }
65
+
66
+ export interface UiAction {
67
+ action: "press" | "click" | "setText" | "typeText" | "keypress" | "scroll" | "drag" | "moveMouse";
68
+ ref?: string;
69
+ x?: number;
70
+ y?: number;
71
+ text?: string;
72
+ keys?: string[];
73
+ scrollX?: number;
74
+ scrollY?: number;
75
+ path?: Array<{ x: number; y: number } | [number, number]>;
76
+ button?: MouseButtonName;
77
+ clickCount?: number;
78
+ }
79
+
80
+ export interface ActParams extends StateTargetParams {
81
+ actions: UiAction[];
82
+ expect?: UiCondition;
83
+ }
84
+
85
+ export interface ReadTextParams extends StateTargetParams {
86
+ ref: string;
87
+ offset?: number;
88
+ }
89
+
90
+ export interface WaitForParams extends StateTargetParams, UiCondition {}
91
+
92
+ export const AGENT_TOOL_NAMES = new Set([
93
+ "find_roots",
94
+ "read_text",
95
+ "wait_for",
96
+ "observe_ui",
97
+ "search_ui",
98
+ "expand_ui",
99
+ "inspect_ui",
100
+ "act_ui",
101
+ "navigate_browser",
102
+ "evaluate_browser",
103
+ "launch_browser",
104
+ ]);
@@ -0,0 +1,195 @@
1
+ import { nodeByRef, outlineNodeLabel, type Outline, type OutlineNode } from "./outline.ts";
2
+
3
+ interface NoteRegion {
4
+ key: string;
5
+ label: string;
6
+ status: "seen" | "changed" | "never-looked";
7
+ detail?: string;
8
+ }
9
+
10
+ export interface WindowNote {
11
+ windowRef: string;
12
+ title: string;
13
+ pairing: "exact" | "high" | "low";
14
+ lastLookId?: string;
15
+ regions: NoteRegion[];
16
+ }
17
+
18
+ interface NoteWindowInput {
19
+ windowRef?: string;
20
+ title: string;
21
+ pairing?: "exact" | "high" | "low";
22
+ pairingScore?: number;
23
+ }
24
+
25
+ interface NoteRefreshOutcome {
26
+ window: NoteWindowInput;
27
+ windowChanged?: boolean;
28
+ newWindowLabel?: string;
29
+ rootDelta?: Array<{ change: string; kind: string; title?: string; ref?: string }>;
30
+ }
31
+
32
+ function normalizedLabel(value: string): string {
33
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
34
+ }
35
+
36
+ function nodeLabel(node: OutlineNode): string {
37
+ return outlineNodeLabel(node) || node.role || node.ref;
38
+ }
39
+
40
+ function regionKey(node: OutlineNode): string {
41
+ return `${node.role || "AXUnknown"}:${node.identifier || normalizedLabel(nodeLabel(node)) || node.ref}`;
42
+ }
43
+
44
+ function regionLabel(node: OutlineNode): string {
45
+ const label = nodeLabel(node);
46
+ return normalizedLabel(label) || normalizedLabel(node.role) || node.ref;
47
+ }
48
+
49
+ function topLevelRegions(outline: Outline): Array<{ node: OutlineNode; key: string; label: string }> {
50
+ const nodes = outline.root.children.length ? outline.root.children : [outline.root];
51
+ return nodes.map((node) => ({ node, key: regionKey(node), label: regionLabel(node) }));
52
+ }
53
+
54
+ function topLevelAncestor(node: OutlineNode, outline: Outline): OutlineNode {
55
+ let current = node;
56
+ while (current.parent && current.parent !== outline.root) current = current.parent;
57
+ return current;
58
+ }
59
+
60
+ function uniqueRegions(regions: NoteRegion[]): NoteRegion[] {
61
+ const seen = new Set<string>();
62
+ const output: NoteRegion[] = [];
63
+ for (const region of regions) {
64
+ const dedupeKey = `${region.key}:${region.status}:${region.detail ?? ""}`;
65
+ if (seen.has(dedupeKey)) continue;
66
+ seen.add(dedupeKey);
67
+ output.push(region);
68
+ }
69
+ return output;
70
+ }
71
+
72
+ function frontierRegions(outline: Outline, topLevels: Array<{ node: OutlineNode; key: string; label: string }>): NoteRegion[] {
73
+ const output: NoteRegion[] = [];
74
+ for (const node of outline.nodes) {
75
+ if (node.scrollExtent && node.scrollExtent.seen < node.scrollExtent.total) {
76
+ const ancestor = topLevelAncestor(node, outline);
77
+ const top = topLevels.find((candidate) => candidate.node === ancestor);
78
+ output.push({
79
+ key: `${top?.key ?? regionKey(ancestor)}:scroll:${node.ref}`,
80
+ label: top?.label ?? regionLabel(ancestor),
81
+ status: "never-looked",
82
+ detail: `${node.scrollExtent.total} rows, ${node.scrollExtent.seen} seen`,
83
+ });
84
+ }
85
+ if (node.truncated) {
86
+ const ancestor = topLevelAncestor(node, outline);
87
+ const top = topLevels.find((candidate) => candidate.node === ancestor);
88
+ output.push({
89
+ key: `${top?.key ?? regionKey(ancestor)}:truncated:${node.ref}`,
90
+ label: top?.label ?? regionLabel(ancestor),
91
+ status: "never-looked",
92
+ detail: "subtree not walked",
93
+ });
94
+ }
95
+ }
96
+ return output;
97
+ }
98
+
99
+ export function noteFromLook(prev: WindowNote | undefined, outline: Outline, window: NoteWindowInput): WindowNote {
100
+ const topLevels = topLevelRegions(outline);
101
+ const currentKeys = new Set(topLevels.map((region) => region.key));
102
+ const regions: NoteRegion[] = topLevels.map((region) => ({
103
+ key: region.key,
104
+ label: region.label,
105
+ status: "seen",
106
+ }));
107
+
108
+ if (prev) {
109
+ for (const old of prev.regions) {
110
+ if (old.status === "never-looked") continue;
111
+ if (!currentKeys.has(old.key)) {
112
+ regions.push({ ...old, status: "changed", detail: "not matched in latest look" });
113
+ }
114
+ }
115
+ }
116
+
117
+ if (!window.windowRef || window.pairingScore === Number.NEGATIVE_INFINITY) {
118
+ regions.push({
119
+ key: "window:unpaired",
120
+ label: "window capture",
121
+ status: "never-looked",
122
+ detail: "AX window without capture pairing",
123
+ });
124
+ }
125
+
126
+ regions.push(...frontierRegions(outline, topLevels));
127
+
128
+ return {
129
+ windowRef: window.windowRef ?? "(unpaired)",
130
+ title: window.title,
131
+ pairing: window.pairing ?? "low",
132
+ lastLookId: outline.lookId,
133
+ regions: uniqueRegions(regions),
134
+ };
135
+ }
136
+
137
+ export function noteAfterAct(prev: WindowNote | undefined, targetRef: string | undefined, outline: Outline, refreshOutcome: NoteRefreshOutcome): WindowNote {
138
+ const note = noteFromLook(prev, outline, refreshOutcome.window);
139
+ if (targetRef) {
140
+ const target = nodeByRef(outline, targetRef);
141
+ if (target) {
142
+ const ancestor = topLevelAncestor(target, outline);
143
+ const key = regionKey(ancestor);
144
+ const region = note.regions.find((candidate) => candidate.key === key);
145
+ if (region) {
146
+ region.status = "changed";
147
+ region.detail = "acted here";
148
+ } else {
149
+ note.regions.unshift({ key, label: regionLabel(ancestor), status: "changed", detail: "acted here" });
150
+ }
151
+ }
152
+ }
153
+ for (const delta of refreshOutcome.rootDelta ?? []) {
154
+ note.regions.unshift({
155
+ key: `root:${delta.change}:${delta.ref ?? delta.kind}:${delta.title ?? ""}`,
156
+ label: `${delta.kind}${delta.title ? ` ${delta.title}` : ""}`,
157
+ status: delta.change === "closed" ? "changed" : "never-looked",
158
+ detail: `root ${delta.change}`,
159
+ });
160
+ }
161
+ if (refreshOutcome.windowChanged) {
162
+ for (const region of note.regions) {
163
+ if (region.status === "seen") region.status = "changed";
164
+ }
165
+ note.regions.push({
166
+ key: `window:changed:${note.lastLookId ?? "unknown"}`,
167
+ label: refreshOutcome.newWindowLabel ?? "new sheet/window",
168
+ status: "never-looked",
169
+ detail: "appeared after act",
170
+ });
171
+ }
172
+ return { ...note, regions: uniqueRegions(note.regions) };
173
+ }
174
+
175
+ export function noteRegionKeyForRef(outline: Outline, ref: string): string | undefined {
176
+ const node = nodeByRef(outline, ref);
177
+ return node ? regionKey(topLevelAncestor(node, outline)) : undefined;
178
+ }
179
+
180
+ export function renderNote(note: WindowNote | undefined): string {
181
+ if (!note) return "";
182
+ const looked = note.lastLookId ? "looked just now" : "not looked";
183
+ const lines = [`note ${note.windowRef} ${JSON.stringify(note.title.slice(0, 512))} (pairing ${note.pairing}, ${looked})`];
184
+ const visible = note.regions.slice(0, 64);
185
+ for (const region of visible) {
186
+ const detail = region.detail ? ` (${region.detail.slice(0, 256)})` : "";
187
+ lines.push(` ${region.label.slice(0, 512).padEnd(14, " ")} ${region.status}${detail}`);
188
+ }
189
+ if (note.regions.length > visible.length) lines.push(` … ${note.regions.length - visible.length} more note regions; use search_ui or expand_ui`);
190
+ const encoded = new TextEncoder().encode(lines.join("\n"));
191
+ if (encoded.byteLength <= 8 * 1024) return new TextDecoder().decode(encoded);
192
+ let end = 8 * 1024;
193
+ while (end > 0 && (encoded[end] & 0xc0) === 0x80) end -= 1;
194
+ return `${new TextDecoder().decode(encoded.subarray(0, end))}\n… note byte budget reached`;
195
+ }