@oh-my-pi/pi-coding-agent 14.5.10 → 14.5.12

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.
@@ -0,0 +1,417 @@
1
+ import * as path from "node:path";
2
+ import { logger } from "@oh-my-pi/pi-utils";
3
+ import type { Subprocess } from "bun";
4
+ import type { Browser, CDPSession, ElementHandle, Page } from "puppeteer-core";
5
+ import { ToolAbortError, ToolError } from "../tool-errors";
6
+ import {
7
+ findFreeCdpPort,
8
+ findReusableCdp,
9
+ gracefulKillTreeOnce,
10
+ killExistingByPath,
11
+ pickElectronTarget,
12
+ waitForCdp,
13
+ } from "./attach";
14
+ import {
15
+ applyStealthPatches,
16
+ applyViewport,
17
+ launchHeadlessBrowser,
18
+ loadPuppeteer,
19
+ type UserAgentOverride,
20
+ } from "./launch";
21
+
22
+ export type BrowserKind =
23
+ | { kind: "headless"; headless: boolean }
24
+ | { kind: "spawned"; path: string }
25
+ | { kind: "connected"; cdpUrl: string };
26
+
27
+ export type BrowserKindTag = BrowserKind["kind"];
28
+
29
+ export interface BrowserHandle {
30
+ key: string;
31
+ kind: BrowserKind;
32
+ browser: Browser;
33
+ cdpUrl?: string;
34
+ pid?: number;
35
+ subprocess?: Subprocess;
36
+ refCount: number;
37
+ stealth: { browserSession: CDPSession | null; override: UserAgentOverride | null };
38
+ }
39
+
40
+ export type DialogPolicy = "accept" | "dismiss";
41
+
42
+ export interface TabHandle {
43
+ name: string;
44
+ browser: BrowserHandle;
45
+ page: Page;
46
+ elementCache: Map<number, ElementHandle>;
47
+ elementCounter: number;
48
+ dialogPolicy?: DialogPolicy;
49
+ dialogHandler?: (dialog: { accept: () => Promise<void>; dismiss: () => Promise<void> }) => void;
50
+ }
51
+
52
+ const browsers = new Map<string, BrowserHandle>();
53
+ const tabs = new Map<string, TabHandle>();
54
+
55
+ export function getTab(name: string): TabHandle | undefined {
56
+ return tabs.get(name);
57
+ }
58
+
59
+ export function listTabs(): TabHandle[] {
60
+ return [...tabs.values()];
61
+ }
62
+
63
+ export function listBrowsers(): BrowserHandle[] {
64
+ return [...browsers.values()];
65
+ }
66
+
67
+ function browserKey(kind: BrowserKind): string {
68
+ switch (kind.kind) {
69
+ case "headless":
70
+ return `headless:${kind.headless ? "1" : "0"}`;
71
+ case "spawned":
72
+ return `spawned:${kind.path}`;
73
+ case "connected":
74
+ return `connected:${kind.cdpUrl}`;
75
+ }
76
+ }
77
+
78
+ export interface AcquireBrowserOptions {
79
+ cwd: string;
80
+ viewport?: { width: number; height: number; deviceScaleFactor?: number };
81
+ appArgs?: string[];
82
+ signal?: AbortSignal;
83
+ }
84
+
85
+ export async function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
86
+ const key = browserKey(kind);
87
+ const existing = browsers.get(key);
88
+ if (existing) {
89
+ // Headless: connection check; spawned/connected: connection check.
90
+ if (existing.browser.connected) return existing;
91
+ // Stale handle — purge and rebuild.
92
+ browsers.delete(key);
93
+ await disposeBrowserHandle(existing, { kill: false });
94
+ }
95
+
96
+ const handle = await openBrowserHandle(kind, opts);
97
+ browsers.set(key, handle);
98
+ return handle;
99
+ }
100
+
101
+ async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
102
+ if (kind.kind === "headless") {
103
+ const browser = await launchHeadlessBrowser({ headless: kind.headless, viewport: opts.viewport });
104
+ return {
105
+ key: browserKey(kind),
106
+ kind,
107
+ browser,
108
+ refCount: 0,
109
+ stealth: { browserSession: null, override: null },
110
+ };
111
+ }
112
+ if (kind.kind === "connected") {
113
+ const cdpUrl = kind.cdpUrl.replace(/\/+$/, "");
114
+ await waitForCdp(cdpUrl, 5_000, opts.signal);
115
+ const puppeteer = await loadPuppeteer();
116
+ const browser = await puppeteer.connect({ browserURL: cdpUrl, defaultViewport: null });
117
+ return {
118
+ key: browserKey(kind),
119
+ kind,
120
+ browser,
121
+ cdpUrl,
122
+ refCount: 0,
123
+ stealth: { browserSession: null, override: null },
124
+ };
125
+ }
126
+ // spawned
127
+ const exe = kind.path;
128
+ if (!path.isAbsolute(exe)) {
129
+ throw new ToolError(
130
+ `app.path must be absolute (got ${JSON.stringify(exe)}). Pass the binary inside Foo.app/Contents/MacOS/, not the .app bundle.`,
131
+ );
132
+ }
133
+ const reused = await findReusableCdp(exe, opts.signal);
134
+ let cdpUrl: string;
135
+ let pid: number;
136
+ let subprocess: Subprocess | undefined;
137
+ if (reused) {
138
+ logger.debug("Reusing existing CDP endpoint for attach", {
139
+ exe,
140
+ pid: reused.pid,
141
+ cdpUrl: reused.cdpUrl,
142
+ });
143
+ cdpUrl = reused.cdpUrl;
144
+ pid = reused.pid;
145
+ } else {
146
+ const killed = await killExistingByPath(exe, opts.signal);
147
+ if (killed > 0) {
148
+ logger.debug("Killed existing instances before attach", { exe, killed });
149
+ }
150
+ const port = await findFreeCdpPort();
151
+ const launchArgs = [...(opts.appArgs ?? []), `--remote-debugging-port=${port}`];
152
+ const child = Bun.spawn([exe, ...launchArgs], {
153
+ stdout: "ignore",
154
+ stderr: "ignore",
155
+ stdin: "ignore",
156
+ });
157
+ child.unref();
158
+ subprocess = child;
159
+ pid = child.pid;
160
+ cdpUrl = `http://127.0.0.1:${port}`;
161
+ try {
162
+ await waitForCdp(cdpUrl, 30_000, opts.signal);
163
+ } catch (err) {
164
+ await gracefulKillTreeOnce(child.pid).catch(() => undefined);
165
+ if (err instanceof ToolAbortError) throw err;
166
+ if (err instanceof Error && err.name === "AbortError") throw err;
167
+ throw new ToolError(`Failed to attach to ${path.basename(exe)} on ${cdpUrl}: ${(err as Error).message}`);
168
+ }
169
+ }
170
+
171
+ const puppeteer = await loadPuppeteer();
172
+ let browser: Browser;
173
+ try {
174
+ browser = await puppeteer.connect({ browserURL: cdpUrl, defaultViewport: null });
175
+ } catch (err) {
176
+ if (subprocess) await gracefulKillTreeOnce(subprocess.pid);
177
+ throw new ToolError(`Connected to ${cdpUrl} but puppeteer.connect failed: ${(err as Error).message}`);
178
+ }
179
+ return {
180
+ key: browserKey(kind),
181
+ kind,
182
+ browser,
183
+ cdpUrl,
184
+ pid,
185
+ subprocess,
186
+ refCount: 0,
187
+ stealth: { browserSession: null, override: null },
188
+ };
189
+ }
190
+
191
+ export interface AcquireTabOptions {
192
+ url?: string;
193
+ waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
194
+ viewport?: { width: number; height: number; deviceScaleFactor?: number };
195
+ target?: string;
196
+ signal?: AbortSignal;
197
+ timeoutMs: number;
198
+ dialogs?: DialogPolicy;
199
+ }
200
+
201
+ export interface AcquireTabResult {
202
+ tab: TabHandle;
203
+ created: boolean;
204
+ }
205
+
206
+ export async function acquireTab(
207
+ name: string,
208
+ browser: BrowserHandle,
209
+ opts: AcquireTabOptions,
210
+ ): Promise<AcquireTabResult> {
211
+ const existing = tabs.get(name);
212
+ if (existing) {
213
+ if (existing.browser !== browser) {
214
+ throw new ToolError(
215
+ `Tab ${JSON.stringify(name)} already exists on a different browser (${existing.browser.kind.kind}). Close it first.`,
216
+ );
217
+ }
218
+ if (!existing.page.isClosed()) {
219
+ if (opts.dialogs !== undefined) applyDialogPolicy(existing, opts.dialogs);
220
+ if (opts.url) {
221
+ clearElementCache(existing);
222
+ await existing.page.goto(opts.url, {
223
+ waitUntil: opts.waitUntil ?? "networkidle2",
224
+ timeout: opts.timeoutMs,
225
+ });
226
+ }
227
+ return { tab: existing, created: false };
228
+ }
229
+ // Stale tab — purge and recreate.
230
+ tabs.delete(name);
231
+ browser.refCount = Math.max(0, browser.refCount - 1);
232
+ }
233
+
234
+ let page: Page;
235
+ if (browser.kind.kind === "headless") {
236
+ page = await browser.browser.newPage();
237
+ await applyStealthPatches(browser.browser, page, browser.stealth);
238
+ if (browser.kind.headless || opts.viewport) {
239
+ await applyViewport(page, opts.viewport);
240
+ }
241
+ } else {
242
+ // spawned/connected — don't open a new tab in the user's app; pick an existing target.
243
+ page = await pickElectronTarget(browser.browser, opts.target);
244
+ }
245
+
246
+ const tab: TabHandle = {
247
+ name,
248
+ browser,
249
+ page,
250
+ elementCache: new Map(),
251
+ elementCounter: 0,
252
+ };
253
+ tabs.set(name, tab);
254
+ browser.refCount++;
255
+ if (opts.dialogs !== undefined) applyDialogPolicy(tab, opts.dialogs);
256
+
257
+ if (opts.url) {
258
+ await page.goto(opts.url, {
259
+ waitUntil: opts.waitUntil ?? "networkidle2",
260
+ timeout: opts.timeoutMs,
261
+ });
262
+ }
263
+
264
+ return { tab, created: true };
265
+ }
266
+
267
+ export interface ReleaseTabOptions {
268
+ kill?: boolean;
269
+ }
270
+
271
+ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Promise<boolean> {
272
+ const tab = tabs.get(name);
273
+ if (!tab) {
274
+ logger.debug("releaseTab: unknown tab", { name });
275
+ return false;
276
+ }
277
+ tabs.delete(name);
278
+ await disposeTab(tab);
279
+ tab.browser.refCount = Math.max(0, tab.browser.refCount - 1);
280
+ if (tab.browser.refCount === 0) {
281
+ browsers.delete(tab.browser.key);
282
+ await disposeBrowserHandle(tab.browser, { kill: opts.kill ?? false });
283
+ }
284
+ return true;
285
+ }
286
+
287
+ export async function releaseAllTabs(opts: ReleaseTabOptions = {}): Promise<number> {
288
+ const names = [...tabs.keys()];
289
+ let count = 0;
290
+ for (const name of names) {
291
+ if (await releaseTab(name, opts)) count++;
292
+ }
293
+ return count;
294
+ }
295
+
296
+ /** Drop only headless browsers and their tabs. Used by the headless-toggle slash command. */
297
+ export async function dropHeadlessBrowsers(): Promise<void> {
298
+ const targets = [...tabs.values()].filter(t => t.browser.kind.kind === "headless");
299
+ for (const tab of targets) {
300
+ await releaseTab(tab.name);
301
+ }
302
+ // Drop any zero-refcount headless browsers that survived (shouldn't happen, defensive).
303
+ for (const [key, browser] of browsers) {
304
+ if (browser.kind.kind === "headless" && browser.refCount === 0) {
305
+ browsers.delete(key);
306
+ await disposeBrowserHandle(browser, { kill: false });
307
+ }
308
+ }
309
+ }
310
+
311
+ function applyDialogPolicy(tab: TabHandle, policy: DialogPolicy): void {
312
+ if (tab.dialogPolicy === policy && tab.dialogHandler) return;
313
+ if (tab.dialogHandler) {
314
+ try {
315
+ tab.page.off("dialog", tab.dialogHandler);
316
+ } catch {}
317
+ }
318
+ const handler = (dialog: { accept: () => Promise<void>; dismiss: () => Promise<void> }): void => {
319
+ const action = policy === "accept" ? dialog.accept() : dialog.dismiss();
320
+ void action.catch(err => {
321
+ logger.debug("Dialog auto-handler failed", { policy, error: (err as Error).message });
322
+ });
323
+ };
324
+ tab.page.on("dialog", handler);
325
+ tab.dialogPolicy = policy;
326
+ tab.dialogHandler = handler;
327
+ }
328
+
329
+ async function disposeTab(tab: TabHandle): Promise<void> {
330
+ clearElementCache(tab);
331
+ if (tab.dialogHandler && !tab.page.isClosed()) {
332
+ try {
333
+ tab.page.off("dialog", tab.dialogHandler);
334
+ } catch {}
335
+ tab.dialogHandler = undefined;
336
+ tab.dialogPolicy = undefined;
337
+ }
338
+ if (tab.browser.kind.kind === "headless") {
339
+ // Owned tab — close it.
340
+ if (!tab.page.isClosed()) {
341
+ try {
342
+ await tab.page.close();
343
+ } catch (err) {
344
+ logger.debug("Failed to close page", { error: (err as Error).message });
345
+ }
346
+ }
347
+ }
348
+ // spawned/connected: page belongs to user's app — never close.
349
+ }
350
+
351
+ async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean }): Promise<void> {
352
+ if (handle.kind.kind === "headless") {
353
+ if (handle.browser.connected) {
354
+ try {
355
+ await handle.browser.close();
356
+ } catch (err) {
357
+ logger.debug("Failed to close headless browser", { error: (err as Error).message });
358
+ }
359
+ }
360
+ return;
361
+ }
362
+ if (handle.kind.kind === "connected") {
363
+ // Never close a remote app — only disconnect.
364
+ if (handle.browser.connected) {
365
+ try {
366
+ handle.browser.disconnect();
367
+ } catch (err) {
368
+ logger.debug("Failed to disconnect from remote browser", { error: (err as Error).message });
369
+ }
370
+ }
371
+ return;
372
+ }
373
+ // spawned
374
+ if (handle.browser.connected) {
375
+ try {
376
+ handle.browser.disconnect();
377
+ } catch (err) {
378
+ logger.debug("Failed to disconnect from spawned browser", { error: (err as Error).message });
379
+ }
380
+ }
381
+ if (opts.kill && handle.pid !== undefined) {
382
+ await gracefulKillTreeOnce(handle.pid);
383
+ }
384
+ }
385
+
386
+ export function clearElementCache(tab: TabHandle): void {
387
+ if (tab.elementCache.size === 0) {
388
+ tab.elementCounter = 0;
389
+ return;
390
+ }
391
+ const handles = [...tab.elementCache.values()];
392
+ tab.elementCache.clear();
393
+ tab.elementCounter = 0;
394
+ for (const handle of handles) {
395
+ // Fire and forget; disposal failures don't affect correctness.
396
+ void handle.dispose().catch(() => undefined);
397
+ }
398
+ }
399
+
400
+ export async function resolveCachedHandle(tab: TabHandle, id: number): Promise<ElementHandle> {
401
+ const handle = tab.elementCache.get(id);
402
+ if (!handle) {
403
+ throw new ToolError(`Unknown element id ${id}. Run tab.observe() to refresh the element list.`);
404
+ }
405
+ try {
406
+ const isConnected = (await handle.evaluate(el => el.isConnected)) as boolean;
407
+ if (!isConnected) {
408
+ clearElementCache(tab);
409
+ throw new ToolError(`Element id ${id} is stale. Run tab.observe() again.`);
410
+ }
411
+ } catch (err) {
412
+ if (err instanceof ToolError) throw err;
413
+ clearElementCache(tab);
414
+ throw new ToolError(`Element id ${id} is stale. Run tab.observe() again.`);
415
+ }
416
+ return handle;
417
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * TUI renderer for the browser tool.
3
+ *
4
+ * Mirrors the `python` tool look: each `run` invocation is shown as a JS code
5
+ * cell with status icon, optional output, and expand/collapse handling. `open`
6
+ * and `close` actions render as compact status lines.
7
+ */
8
+ import type { Component } from "@oh-my-pi/pi-tui";
9
+ import { Text } from "@oh-my-pi/pi-tui";
10
+ import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
11
+ import type { Theme } from "../../modes/theme/theme";
12
+ import { Hasher, renderCodeCell, renderStatusLine } from "../../tui";
13
+ import type { BrowserToolDetails } from "../browser";
14
+ import { formatStyledTruncationWarning } from "../output-meta";
15
+ import { replaceTabs, shortenPath } from "../render-utils";
16
+
17
+ const BROWSER_DEFAULT_PREVIEW_LINES = 10;
18
+
19
+ interface BrowserRenderArgs {
20
+ action?: "open" | "close" | "run";
21
+ name?: string;
22
+ url?: string;
23
+ code?: string;
24
+ all?: boolean;
25
+ kill?: boolean;
26
+ app?: { path?: string; cdp_url?: string; target?: string };
27
+ viewport?: { width: number; height: number; scale?: number };
28
+ timeout?: number;
29
+ }
30
+
31
+ interface BrowserRenderContext {
32
+ expanded?: boolean;
33
+ previewLines?: number;
34
+ }
35
+
36
+ function describeBrowser(args: BrowserRenderArgs, details: BrowserToolDetails | undefined): string | undefined {
37
+ if (args.app?.cdp_url) return `connected ${args.app.cdp_url}`;
38
+ if (args.app?.path) return `spawned ${shortenPath(args.app.path)}`;
39
+ switch (details?.browser) {
40
+ case "headless":
41
+ return "headless";
42
+ case "spawned":
43
+ return "spawned";
44
+ case "connected":
45
+ return "connected";
46
+ default:
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ function tabLabel(args: BrowserRenderArgs, details: BrowserToolDetails | undefined): string {
52
+ const name = details?.name ?? args.name ?? "main";
53
+ return `tab ${JSON.stringify(name)}`;
54
+ }
55
+
56
+ function cellStatus(isPartial: boolean, isError: boolean): "pending" | "running" | "complete" | "error" {
57
+ if (isPartial) return "running";
58
+ if (isError) return "error";
59
+ return "complete";
60
+ }
61
+
62
+ function dropTrailingBlankLines(text: string): string {
63
+ return text.replace(/\s+$/, "");
64
+ }
65
+
66
+ function appendLine(component: Component, line: string | undefined): Component {
67
+ if (!line) return component;
68
+ return {
69
+ render: (width: number): string[] => {
70
+ const base = component.render(width);
71
+ return [...base, line];
72
+ },
73
+ invalidate: () => component.invalidate?.(),
74
+ };
75
+ }
76
+
77
+ function renderRunCell(
78
+ args: BrowserRenderArgs,
79
+ details: BrowserToolDetails | undefined,
80
+ options: RenderResultOptions & { renderContext?: BrowserRenderContext },
81
+ output: string,
82
+ isError: boolean,
83
+ theme: Theme,
84
+ ): Component {
85
+ const code = dropTrailingBlankLines(args.code ?? "");
86
+ const status = cellStatus(options.isPartial, isError);
87
+
88
+ const titleParts: string[] = [tabLabel(args, details)];
89
+ const url = details?.url ?? args.url;
90
+ if (url) titleParts.push(shortenPath(url));
91
+ const browserDesc = describeBrowser(args, details);
92
+ if (browserDesc) titleParts.push(browserDesc);
93
+ const title = titleParts.join(" · ");
94
+
95
+ let cached: { key: bigint; width: number; lines: string[] } | undefined;
96
+ return {
97
+ render: (width: number): string[] => {
98
+ const expanded = options.renderContext?.expanded ?? options.expanded;
99
+ const previewLines = options.renderContext?.previewLines ?? BROWSER_DEFAULT_PREVIEW_LINES;
100
+ const key = new Hasher()
101
+ .bool(expanded)
102
+ .bool(isError)
103
+ .u32(previewLines)
104
+ .u32(options.spinnerFrame ?? 0)
105
+ .str(status)
106
+ .str(title)
107
+ .str(code)
108
+ .str(output)
109
+ .digest();
110
+ if (cached && cached.width === width && cached.key === key) {
111
+ return cached.lines;
112
+ }
113
+ const lines = renderCodeCell(
114
+ {
115
+ code,
116
+ language: "javascript",
117
+ title,
118
+ status,
119
+ spinnerFrame: options.spinnerFrame,
120
+ output: output.length > 0 ? output : undefined,
121
+ outputMaxLines: expanded ? Number.POSITIVE_INFINITY : previewLines,
122
+ codeMaxLines: expanded ? Number.POSITIVE_INFINITY : previewLines,
123
+ expanded,
124
+ width,
125
+ },
126
+ theme,
127
+ );
128
+ cached = { key, width, lines };
129
+ return lines;
130
+ },
131
+ invalidate: () => {
132
+ cached = undefined;
133
+ },
134
+ };
135
+ }
136
+
137
+ function renderOpenOrCloseLine(
138
+ args: BrowserRenderArgs,
139
+ details: BrowserToolDetails | undefined,
140
+ isPartial: boolean,
141
+ isError: boolean,
142
+ output: string,
143
+ theme: Theme,
144
+ ): Component {
145
+ const action = (details?.action ?? args.action ?? "open") as "open" | "close" | "run";
146
+ const status = cellStatus(isPartial, isError);
147
+ const icon =
148
+ status === "complete" ? "success" : status === "error" ? "error" : status === "running" ? "running" : "pending";
149
+
150
+ let title: string;
151
+ if (action === "close") {
152
+ const all = args.all === true || (args.name === undefined && details?.name === undefined);
153
+ title = all ? "Close all tabs" : `Close ${tabLabel(args, details)}`;
154
+ if (args.kill) title += " (kill)";
155
+ } else {
156
+ title = `Open ${tabLabel(args, details)}`;
157
+ }
158
+
159
+ const meta: string[] = [];
160
+ const browserDesc = describeBrowser(args, details);
161
+ if (browserDesc) meta.push(browserDesc);
162
+ const url = details?.url ?? args.url;
163
+ if (url) meta.push(shortenPath(url));
164
+
165
+ const header = renderStatusLine({ icon, title, meta }, theme);
166
+ if (!output) return new Text(header, 0, 0);
167
+ const outputLines = output.split("\n").map(line => theme.fg("toolOutput", replaceTabs(line)));
168
+ return new Text([header, ...outputLines].join("\n"), 0, 0);
169
+ }
170
+
171
+ function extractTextOutput(content: Array<{ type: string; text?: string }> | undefined): string {
172
+ if (!content) return "";
173
+ const text = content
174
+ .filter(c => c.type === "text")
175
+ .map(c => c.text ?? "")
176
+ .join("\n");
177
+ return dropTrailingBlankLines(text);
178
+ }
179
+
180
+ export const browserToolRenderer = {
181
+ renderCall(args: BrowserRenderArgs, options: RenderResultOptions, theme: Theme): Component {
182
+ const action = args.action;
183
+ if (action === "run") {
184
+ return renderRunCell(args, undefined, options, "", false, theme);
185
+ }
186
+ return renderOpenOrCloseLine(args, undefined, options.isPartial, false, "", theme);
187
+ },
188
+ renderResult(
189
+ result: { content: Array<{ type: string; text?: string }>; details?: BrowserToolDetails; isError?: boolean },
190
+ options: RenderResultOptions & { renderContext?: BrowserRenderContext },
191
+ theme: Theme,
192
+ args?: BrowserRenderArgs,
193
+ ): Component {
194
+ const argsObj = args ?? {};
195
+ const details = result.details;
196
+ const action = details?.action ?? argsObj.action;
197
+ const isError = result.isError === true;
198
+ const output = extractTextOutput(result.content);
199
+
200
+ if (action === "run") {
201
+ let component = renderRunCell(argsObj, details, options, output, isError, theme);
202
+ const truncationWarning = details?.meta?.truncation
203
+ ? (formatStyledTruncationWarning(details.meta, theme) ?? undefined)
204
+ : undefined;
205
+ component = appendLine(component, truncationWarning);
206
+ return component;
207
+ }
208
+ return renderOpenOrCloseLine(argsObj, details, options.isPartial, isError, output, theme);
209
+ },
210
+ mergeCallAndResult: true,
211
+ inline: true,
212
+ };