pi-pretty-tui 0.3.0 → 0.3.2
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 +41 -87
- package/README.zh-CN.md +40 -86
- package/assets/screenshot.png +0 -0
- package/extensions/full-frame.ts +16 -5
- package/extensions/index.ts +192 -87
- package/extensions/markdown-emphasis.ts +87 -0
- package/extensions/markdown-spacing.ts +123 -0
- package/extensions/selection-ranges.ts +245 -0
- package/extensions/tool-presentation.ts +8 -2
- package/package.json +10 -4
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { installSelectionCopy, selectableRow, selectionMetadataActive } from "./selection-ranges.js";
|
|
1
2
|
import { isNativeImageRow } from "./ansi-style.js";
|
|
2
3
|
import { cardMargin, cardBackground, frameNativeRows, nativeFrameWidth } from "./full-frame.js";
|
|
4
|
+
import {
|
|
5
|
+
themedInlineContext,
|
|
6
|
+
withNativeEmphasis,
|
|
7
|
+
type InlineStyleContext,
|
|
8
|
+
type InlineToken,
|
|
9
|
+
} from "./markdown-emphasis.js";
|
|
10
|
+
import { gapRowsAfterBlock, renderSpacedList } from "./markdown-spacing.js";
|
|
3
11
|
import { ThoughtPresentation } from "./thought-presentation.js";
|
|
4
|
-
import { isThirdPartyTool, renderToolProjection, toolLabel, type ToolProjection } from "./tool-presentation.js";
|
|
12
|
+
import { isThirdPartyTool, renderToolProjection, THIRD_PARTY_TOOL, toolLabel, type ToolProjection } from "./tool-presentation.js";
|
|
5
13
|
import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
|
|
6
14
|
import {
|
|
7
15
|
AssistantMessageComponent,
|
|
@@ -62,6 +70,10 @@ const RESPONSE_FOOTER_ENTRY_TYPE = "pretty-tui-response-footer";
|
|
|
62
70
|
type PrettyTuiConfig = {
|
|
63
71
|
enabled?: boolean;
|
|
64
72
|
mode?: PrettyTuiMode;
|
|
73
|
+
/** Exclude plugin decoration from fullscreen selections. Opt-in. */
|
|
74
|
+
selectionCopy?: boolean;
|
|
75
|
+
/** "native" keeps Pi's flags-only strong and emphasis. Read when the extension loads. */
|
|
76
|
+
markdownEmphasis?: "themed" | "native";
|
|
65
77
|
/** Legacy location used by the first mode implementation. */
|
|
66
78
|
bash?: {
|
|
67
79
|
mode?: "full" | "compact";
|
|
@@ -300,6 +312,17 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
300
312
|
}
|
|
301
313
|
};
|
|
302
314
|
const transcriptMarkdownActive = () => transcriptMarkdownDepth > 0;
|
|
315
|
+
// Assistant answers only. The transcript scope also covers user messages,
|
|
316
|
+
// thinking and activity updates, which keep Pi's strong and emphasis styling.
|
|
317
|
+
const assistantBodyMarkdown = new WeakSet<object>();
|
|
318
|
+
const markAssistantBodyMarkdown = (component: any) => {
|
|
319
|
+
// Pi adds answer text directly; a thought block is wrapped in a MouseRegion.
|
|
320
|
+
// A component built before this runtime is covered too: the rendered-mode
|
|
321
|
+
// symbols are per runtime, so its first render here re-runs updateContent.
|
|
322
|
+
for (const child of component.contentContainer?.children ?? []) {
|
|
323
|
+
if (child instanceof Markdown && !(child as any).defaultTextStyle) assistantBodyMarkdown.add(child);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
303
326
|
|
|
304
327
|
const setCleanGroupMembers = (lastToolCallId: string, toolCallIds: string[]) => {
|
|
305
328
|
cleanGroupToolCallIds.set(lastToolCallId, toolCallIds);
|
|
@@ -676,7 +699,9 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
676
699
|
{ value: "full", label: "full", description: "Full tool details and output" },
|
|
677
700
|
{ value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
|
|
678
701
|
{ value: "clean", label: "clean", description: "Group supported tools into Running/Done status" },
|
|
679
|
-
{ value: "
|
|
702
|
+
{ value: "selection-copy on", label: "selection-copy on", description: "Copy selected text without UI borders in fullscreen mode; enable and reload" },
|
|
703
|
+
{ value: "selection-copy off", label: "selection-copy off", description: "Restore native selection copying and reload" },
|
|
704
|
+
{ value: "status", label: "status", description: "Show the current settings" },
|
|
680
705
|
];
|
|
681
706
|
const filtered = items.filter((item) => item.value.startsWith(prefix.trim().toLowerCase()));
|
|
682
707
|
return filtered.length > 0 ? filtered : null;
|
|
@@ -687,7 +712,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
687
712
|
if (!requested) {
|
|
688
713
|
if (!ctx.hasUI) {
|
|
689
714
|
ctx.ui.notify(
|
|
690
|
-
`pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
|
|
715
|
+
`pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
|
|
691
716
|
"info",
|
|
692
717
|
);
|
|
693
718
|
return;
|
|
@@ -696,9 +721,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
696
721
|
const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
|
|
697
722
|
const compact = `Compact — concise tool summaries and nested calls${renderMode === "compact" ? " (current)" : ""}`;
|
|
698
723
|
const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
|
|
699
|
-
const
|
|
724
|
+
const selectionCopy = `Selection copy — ${config.selectionCopy === true ? "on" : "off"} · copy text without UI borders (fullscreen, experimental)`;
|
|
725
|
+
const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean, selectionCopy]);
|
|
700
726
|
if (!selected) return;
|
|
701
|
-
requested = selected ===
|
|
727
|
+
requested = selected === selectionCopy
|
|
728
|
+
? config.selectionCopy === true ? "selection-copy off" : "selection-copy on"
|
|
729
|
+
: selected === enabled
|
|
702
730
|
? config.enabled !== false ? "disable" : "enable"
|
|
703
731
|
: selected === full
|
|
704
732
|
? "full"
|
|
@@ -709,11 +737,28 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
709
737
|
|
|
710
738
|
if (requested === "status") {
|
|
711
739
|
ctx.ui.notify(
|
|
712
|
-
`pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
|
|
740
|
+
`pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
|
|
713
741
|
"info",
|
|
714
742
|
);
|
|
715
743
|
return;
|
|
716
744
|
}
|
|
745
|
+
if (requested === "selection-copy on" || requested === "selection-copy off") {
|
|
746
|
+
const selectionCopy = requested.endsWith(" on");
|
|
747
|
+
try {
|
|
748
|
+
saveConfig({ ...config, selectionCopy });
|
|
749
|
+
} catch (error) {
|
|
750
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
751
|
+
ctx.ui.notify(`Could not save selection-copy setting: ${message}`, "error");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
ctx.ui.notify(`Border-free selection copying ${selectionCopy ? "enabled" : "disabled"}; reloading…`, "info");
|
|
755
|
+
try { await ctx.reload(); }
|
|
756
|
+
catch (error) {
|
|
757
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
758
|
+
ctx.ui.notify(`Setting saved, but automatic reload failed: ${message}`, "warning");
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
717
762
|
if (requested === "enable" || requested === "disable") {
|
|
718
763
|
const enabled = requested === "enable";
|
|
719
764
|
try {
|
|
@@ -737,7 +782,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
737
782
|
}
|
|
738
783
|
}
|
|
739
784
|
if (requested !== "full" && requested !== "compact" && requested !== "clean") {
|
|
740
|
-
ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status]", "error");
|
|
785
|
+
ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status|selection-copy on|selection-copy off]", "error");
|
|
741
786
|
return;
|
|
742
787
|
}
|
|
743
788
|
|
|
@@ -764,6 +809,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
764
809
|
// render patches or override built-in tools until the next enabled reload.
|
|
765
810
|
if (!activeForSession) return;
|
|
766
811
|
pi.on("session_shutdown", stopWorkingElapsedTimer);
|
|
812
|
+
if (config.selectionCopy === true) pi.on("session_shutdown", installSelectionCopy());
|
|
767
813
|
|
|
768
814
|
// Give Pi's main prompt editor a complete rounded frame. Render the native
|
|
769
815
|
// editor at a two-column narrower width so cursor layout, wrapping, IME, and
|
|
@@ -812,9 +858,9 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
812
858
|
|
|
813
859
|
return lines.map((line, index) => {
|
|
814
860
|
const fitted = fitInnerWidth(line);
|
|
815
|
-
if (index === 0) return this.borderColor("╭") + fitted + this.borderColor("╮");
|
|
816
|
-
if (index === bottomBorderIndex) return this.borderColor("╰") + fitted + this.borderColor("╯");
|
|
817
|
-
if (index < bottomBorderIndex) return this.borderColor("│") + fitted + this.borderColor("│");
|
|
861
|
+
if (index === 0) return selectableRow(this.borderColor("╭") + fitted + this.borderColor("╮"), width, []);
|
|
862
|
+
if (index === bottomBorderIndex) return selectableRow(this.borderColor("╰") + fitted + this.borderColor("╯"), width, []);
|
|
863
|
+
if (index < bottomBorderIndex) return selectableRow(this.borderColor("│") + fitted + this.borderColor("│"), width, [[1, width - 1]]);
|
|
818
864
|
// Autocomplete remains outside the input frame, aligned to its content.
|
|
819
865
|
return " " + fitted + " ";
|
|
820
866
|
});
|
|
@@ -980,9 +1026,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
980
1026
|
const bottom = border("╰" + "─".repeat(Math.max(0, measured.frameWidth - 2)) + "╯");
|
|
981
1027
|
const body = measured.body.map((line: string) => {
|
|
982
1028
|
const padding = " ".repeat(Math.max(0, measured.contentWidth - visibleWidth(line)));
|
|
983
|
-
return border("│ ") + line + padding + border(" │");
|
|
1029
|
+
return selectableRow(border("│ ") + line + padding + border(" │"), measured.frameWidth, [[2, 2 + Math.max(1, Math.min(measured.contentWidth, visibleWidth(line)))]]);
|
|
984
1030
|
});
|
|
985
|
-
return [top,
|
|
1031
|
+
return [selectableRow(top, measured.frameWidth, []), ...body, selectableRow(bottom, measured.frameWidth, [])]
|
|
1032
|
+
.map((line: string) => selectableRow(left + line + right, width, [[measured.leftPad, width - measured.rightPad]]));
|
|
986
1033
|
},
|
|
987
1034
|
handleMouse(event: any) {
|
|
988
1035
|
const measured = layout(event.width);
|
|
@@ -1378,6 +1425,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1378
1425
|
originalUpdateContent.call(this, renderMode === "compact" && assistantTerminalState(message)
|
|
1379
1426
|
? { ...message, errorMessage: undefined, stopReason: "stop" } : message, isStreaming);
|
|
1380
1427
|
this.lastMessage = message;
|
|
1428
|
+
markAssistantBodyMarkdown(this);
|
|
1381
1429
|
if (eligibleThinking(message)) thinkingComponents.set(assistantMessageKey(message), this);
|
|
1382
1430
|
return;
|
|
1383
1431
|
}
|
|
@@ -1405,6 +1453,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1405
1453
|
this.hideThinkingBlock = previousHideThinkingBlock;
|
|
1406
1454
|
}
|
|
1407
1455
|
this.lastMessage = message;
|
|
1456
|
+
markAssistantBodyMarkdown(this);
|
|
1408
1457
|
};
|
|
1409
1458
|
|
|
1410
1459
|
const renderAssistantTranscript = function (this: any, width: number): string[] {
|
|
@@ -2261,6 +2310,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2261
2310
|
const codeBlockRegionsKey = Symbol("pretty-tui.code-block-regions");
|
|
2262
2311
|
const transcriptMarkdownStateKey = Symbol("pretty-tui.transcript-markdown-state");
|
|
2263
2312
|
if (!markdownPrototype[codeBlockPatchKey]) {
|
|
2313
|
+
// Pi 1.5 reuses rendered top-level blocks while text streams. Keep copy
|
|
2314
|
+
// descriptors with each native block's highlighting-cache identity, weakly,
|
|
2315
|
+
// so reused pixels retain their hit regions without disabling that cache.
|
|
2316
|
+
const blockCopyDescriptors = new WeakMap<object, { generation: number; blocks: any[] }>();
|
|
2317
|
+
let copyRenderGeneration = 0;
|
|
2264
2318
|
const originalInvalidate = markdownPrototype.invalidate;
|
|
2265
2319
|
const originalRender = markdownPrototype.render;
|
|
2266
2320
|
const originalRenderToken = markdownPrototype.renderToken;
|
|
@@ -2274,6 +2328,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2274
2328
|
return originalInvalidate.call(this);
|
|
2275
2329
|
};
|
|
2276
2330
|
|
|
2331
|
+
const copyRows = new WeakMap<object, { source: string[]; padding: number; lines: string[] }>();
|
|
2332
|
+
const selectableMarkdown = (owner: any, source: string[]) => {
|
|
2333
|
+
if (!selectionMetadataActive()) return source;
|
|
2334
|
+
const padding = Math.max(0, Number(owner.paddingX) || 0);
|
|
2335
|
+
if (!padding) return source;
|
|
2336
|
+
const cached = copyRows.get(owner);
|
|
2337
|
+
if (cached?.source === source && cached.padding === padding) return cached.lines;
|
|
2338
|
+
const lines = source.map(line => {
|
|
2339
|
+
const width = visibleWidth(line);
|
|
2340
|
+
return selectableRow(line, width, [[Math.min(padding, width), Math.max(Math.min(padding, width), width - padding)]]);
|
|
2341
|
+
});
|
|
2342
|
+
copyRows.set(owner, { source, padding, lines });
|
|
2343
|
+
return lines;
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2277
2346
|
const patchedRender = function (this: any, width: number): string[] {
|
|
2278
2347
|
const scoped = transcriptMarkdownActive();
|
|
2279
2348
|
mouseClickHitTests.set(this, (event) => fullscreenTui &&
|
|
@@ -2290,14 +2359,22 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2290
2359
|
|
|
2291
2360
|
const needsRender =
|
|
2292
2361
|
!this.cachedLines || this.cachedText !== this.text || this.cachedWidth !== width;
|
|
2293
|
-
if (needsRender) this[codeBlockCollectionKey] = [];
|
|
2362
|
+
if (needsRender) this[codeBlockCollectionKey] = Object.assign([], { generation: ++copyRenderGeneration });
|
|
2294
2363
|
|
|
2295
2364
|
const lines = originalRender.call(this, width);
|
|
2296
|
-
if (!needsRender) return lines;
|
|
2365
|
+
if (!needsRender) return selectableMarkdown(this, lines);
|
|
2297
2366
|
|
|
2298
2367
|
const regions: Array<{ xStart: number; xEnd: number; y: number; code: string }> = [];
|
|
2299
2368
|
let searchFrom = 0;
|
|
2300
|
-
|
|
2369
|
+
const nativeBlocks = this.blocks?.deref();
|
|
2370
|
+
const nativeTokens = this.cachedTokens?.deref()?.tokens;
|
|
2371
|
+
const copyBlocks = nativeBlocks && nativeTokens
|
|
2372
|
+
? nativeTokens.flatMap((token: any) => {
|
|
2373
|
+
const block = nativeBlocks.get(token.raw);
|
|
2374
|
+
return block ? blockCopyDescriptors.get(block.highlightedCode)?.blocks ?? [] : [];
|
|
2375
|
+
})
|
|
2376
|
+
: this[codeBlockCollectionKey] ?? [];
|
|
2377
|
+
for (const block of copyBlocks) {
|
|
2301
2378
|
let headerLine = -1;
|
|
2302
2379
|
for (let lineIndex = searchFrom; lineIndex < lines.length; lineIndex += 1) {
|
|
2303
2380
|
const renderedLine = lines[lineIndex] ?? "";
|
|
@@ -2321,7 +2398,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2321
2398
|
}
|
|
2322
2399
|
this[codeBlockRegionsKey] = regions;
|
|
2323
2400
|
delete this[codeBlockCollectionKey];
|
|
2324
|
-
return lines;
|
|
2401
|
+
return selectableMarkdown(this, lines);
|
|
2325
2402
|
};
|
|
2326
2403
|
|
|
2327
2404
|
const patchedRenderToken = function (
|
|
@@ -2343,36 +2420,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2343
2420
|
return lines;
|
|
2344
2421
|
};
|
|
2345
2422
|
const headingStyle = (text: string) => this.theme.heading(text);
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
if (!trueColor) return undefined;
|
|
2349
|
-
const [, red, green, blue] = trueColor.map(Number);
|
|
2350
|
-
return red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
|
2351
|
-
};
|
|
2352
|
-
const defaultLuminance = foregroundLuminance(this.applyDefaultStyle("M"));
|
|
2353
|
-
const headingLuminance = foregroundLuminance(headingStyle("M"));
|
|
2354
|
-
const darkTheme =
|
|
2355
|
-
defaultLuminance !== undefined
|
|
2356
|
-
? defaultLuminance >= 128
|
|
2357
|
-
: (headingLuminance ?? 255) >= 160;
|
|
2358
|
-
const whiteStyle = (text: string) => `\x1b[97m${text}\x1b[39m`;
|
|
2423
|
+
// Descending emphasis: framed H1, underlined H2, bold H3,
|
|
2424
|
+
// plain H4, decorative H5, and quote-colored italic H6.
|
|
2359
2425
|
const titleStyle =
|
|
2360
|
-
level === 1 || level ===
|
|
2426
|
+
level === 1 || level === 2
|
|
2361
2427
|
? (text: string) => headingStyle(this.theme.bold(this.theme.underline(text)))
|
|
2362
|
-
: level ===
|
|
2363
|
-
? (text: string) =>
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
? (text: string) => headingStyle(this.theme.bold(text))
|
|
2369
|
-
: level === 5
|
|
2370
|
-
? headingStyle
|
|
2371
|
-
: (text: string) => this.theme.quote(this.theme.italic(text));
|
|
2372
|
-
const titleContext = {
|
|
2428
|
+
: level === 3
|
|
2429
|
+
? (text: string) => headingStyle(this.theme.bold(text))
|
|
2430
|
+
: level === 4 || level === 5
|
|
2431
|
+
? headingStyle
|
|
2432
|
+
: (text: string) => this.theme.quote(this.theme.italic(text));
|
|
2433
|
+
const titleContext = withNativeEmphasis({
|
|
2373
2434
|
applyText: titleStyle,
|
|
2374
2435
|
stylePrefix: this.getStylePrefix(titleStyle),
|
|
2375
|
-
};
|
|
2436
|
+
});
|
|
2376
2437
|
const title = this.renderInlineTokens(token.tokens || [], titleContext);
|
|
2377
2438
|
|
|
2378
2439
|
if (level === 1 && maxWidth >= 6) {
|
|
@@ -2395,17 +2456,6 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2395
2456
|
return addHeadingSpacing(lines);
|
|
2396
2457
|
}
|
|
2397
2458
|
|
|
2398
|
-
if (level === 2) {
|
|
2399
|
-
// Reverse only the rendered title cells: the theme's heading color
|
|
2400
|
-
// becomes a compact, content-width background label with no frame.
|
|
2401
|
-
const lines = wrapTextWithAnsi(title, maxWidth).map((line: string) =>
|
|
2402
|
-
darkTheme
|
|
2403
|
-
? `\x1b[48;5;94m${line}\x1b[49m`
|
|
2404
|
-
: `\x1b[107m\x1b[7m${line}\x1b[27m\x1b[49m`
|
|
2405
|
-
);
|
|
2406
|
-
return addHeadingSpacing(lines);
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
2459
|
if (level === 5 && maxWidth >= 8) {
|
|
2410
2460
|
const prefix = "┄┄ ";
|
|
2411
2461
|
const suffix = " ┄┄";
|
|
@@ -2426,7 +2476,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2426
2476
|
}
|
|
2427
2477
|
|
|
2428
2478
|
if (token?.type !== "code") {
|
|
2429
|
-
return
|
|
2479
|
+
return [
|
|
2480
|
+
...originalRenderToken.call(this, token, width, nextTokenType, styleContext),
|
|
2481
|
+
...gapRowsAfterBlock(token?.type, nextTokenType),
|
|
2482
|
+
];
|
|
2430
2483
|
}
|
|
2431
2484
|
|
|
2432
2485
|
const maxWidth = Math.max(1, width);
|
|
@@ -2479,24 +2532,32 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2479
2532
|
(showCopyButton ? this.theme.codeBlockBorder(` ${copyLabel} `) : "") + frameBorder("╮");
|
|
2480
2533
|
const framedCodeWidth = Math.max(1, frameWidth - 4);
|
|
2481
2534
|
const lines = [
|
|
2482
|
-
styledTopRule,
|
|
2483
|
-
...codeLines.map((line) =>
|
|
2484
|
-
frameBorder("│ ") +
|
|
2485
|
-
line +
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
),
|
|
2489
|
-
frameBorder(`╰${"─".repeat(Math.max(0, frameWidth - 2))}╯`),
|
|
2535
|
+
selectableRow(styledTopRule, frameWidth, []),
|
|
2536
|
+
...codeLines.map((line) => selectableRow(
|
|
2537
|
+
frameBorder("│ ") + line +
|
|
2538
|
+
" ".repeat(Math.max(0, framedCodeWidth - visibleWidth(line))) + frameBorder(" │"),
|
|
2539
|
+
frameWidth, [[2, frameWidth - 2]])),
|
|
2540
|
+
selectableRow(frameBorder(`╰${"─".repeat(Math.max(0, frameWidth - 2))}╯`), frameWidth, []),
|
|
2490
2541
|
];
|
|
2491
2542
|
|
|
2492
2543
|
if (showCopyButton && Array.isArray(this[codeBlockCollectionKey])) {
|
|
2493
|
-
|
|
2544
|
+
const descriptor = {
|
|
2494
2545
|
buttonStart,
|
|
2495
2546
|
buttonWidth: visibleWidth(copyLabel),
|
|
2496
2547
|
code,
|
|
2497
2548
|
styledTopRule,
|
|
2498
2549
|
topRule,
|
|
2499
|
-
}
|
|
2550
|
+
};
|
|
2551
|
+
const collection = this[codeBlockCollectionKey] as any[] & { generation: number };
|
|
2552
|
+
collection.push(descriptor);
|
|
2553
|
+
if (this.blockHighlightedCode) {
|
|
2554
|
+
let cached = blockCopyDescriptors.get(this.blockHighlightedCode);
|
|
2555
|
+
if (!cached || cached.generation !== collection.generation) {
|
|
2556
|
+
cached = { generation: collection.generation, blocks: [] };
|
|
2557
|
+
blockCopyDescriptors.set(this.blockHighlightedCode, cached);
|
|
2558
|
+
}
|
|
2559
|
+
cached.blocks.push(descriptor);
|
|
2560
|
+
}
|
|
2500
2561
|
}
|
|
2501
2562
|
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
2502
2563
|
return lines;
|
|
@@ -2581,7 +2642,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2581
2642
|
this.theme.listBullet = (marker: string) =>
|
|
2582
2643
|
originalListBullet(marker.replace(/^- /, "• "));
|
|
2583
2644
|
try {
|
|
2584
|
-
|
|
2645
|
+
if (!transcriptMarkdownActive()) {
|
|
2646
|
+
return originalRenderList.call(this, token, depth, width, styleContext);
|
|
2647
|
+
}
|
|
2648
|
+
return renderSpacedList(token, (singleItemList) =>
|
|
2649
|
+
originalRenderList.call(this, singleItemList, depth, width, styleContext));
|
|
2585
2650
|
} finally {
|
|
2586
2651
|
this.theme.listBullet = originalListBullet;
|
|
2587
2652
|
}
|
|
@@ -2599,6 +2664,37 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2599
2664
|
});
|
|
2600
2665
|
}
|
|
2601
2666
|
|
|
2667
|
+
const inlineEmphasisPatchKey = Symbol.for("pretty-tui.inline-emphasis");
|
|
2668
|
+
if (config.markdownEmphasis !== "native" && !markdownPrototype[inlineEmphasisPatchKey]) {
|
|
2669
|
+
const originalRenderInlineTokens = markdownPrototype.renderInlineTokens;
|
|
2670
|
+
const patchedRenderInlineTokens = function (
|
|
2671
|
+
this: any,
|
|
2672
|
+
tokens: InlineToken[],
|
|
2673
|
+
styleContext?: InlineStyleContext,
|
|
2674
|
+
): string {
|
|
2675
|
+
const theme = currentExtensionUi?.theme;
|
|
2676
|
+
if (!transcriptMarkdownActive() || !assistantBodyMarkdown.has(this) || !theme?.fg) {
|
|
2677
|
+
return originalRenderInlineTokens.call(this, tokens, styleContext);
|
|
2678
|
+
}
|
|
2679
|
+
return originalRenderInlineTokens.call(
|
|
2680
|
+
this,
|
|
2681
|
+
tokens,
|
|
2682
|
+
themedInlineContext(tokens, styleContext ?? this.getDefaultInlineStyleContext(), theme),
|
|
2683
|
+
);
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2686
|
+
markdownPrototype[inlineEmphasisPatchKey] = { originalRenderInlineTokens, patchedRenderInlineTokens };
|
|
2687
|
+
markdownPrototype.renderInlineTokens = patchedRenderInlineTokens;
|
|
2688
|
+
|
|
2689
|
+
pi.on("session_shutdown", () => {
|
|
2690
|
+
const patch = markdownPrototype[inlineEmphasisPatchKey];
|
|
2691
|
+
if (patch?.patchedRenderInlineTokens === markdownPrototype.renderInlineTokens) {
|
|
2692
|
+
markdownPrototype.renderInlineTokens = patch.originalRenderInlineTokens;
|
|
2693
|
+
}
|
|
2694
|
+
if (patch) delete markdownPrototype[inlineEmphasisPatchKey];
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2602
2698
|
const textContent = (result: any): string =>
|
|
2603
2699
|
result.content
|
|
2604
2700
|
?.filter((item: any) => item.type === "text")
|
|
@@ -3182,13 +3278,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3182
3278
|
if (lines.length === 0) return lines;
|
|
3183
3279
|
contentLines = lines[0] === "" ? lines.slice(1) : lines;
|
|
3184
3280
|
}
|
|
3185
|
-
decoratedContent = contentLines.map((line: string, index: number) =>
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
);
|
|
3281
|
+
decoratedContent = contentLines.map((line: string, index: number) => {
|
|
3282
|
+
const prefix = index === 0 ? childPrefix : continuation;
|
|
3283
|
+
const rendered = truncateToWidth(prefix + line, Math.max(1, width), "");
|
|
3284
|
+
return selectionMetadataActive()
|
|
3285
|
+
? selectableRow(rendered, visibleWidth(rendered), [[visibleWidth(prefix), visibleWidth(rendered)]]) : rendered;
|
|
3286
|
+
});
|
|
3192
3287
|
if (cacheable) {
|
|
3193
3288
|
this[cleanChildRenderCacheKey] = {
|
|
3194
3289
|
width,
|
|
@@ -4333,21 +4428,31 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4333
4428
|
grep: createGrepToolDefinition(cwd), find: createFindToolDefinition(cwd), ls: createLsToolDefinition(cwd),
|
|
4334
4429
|
};
|
|
4335
4430
|
const registerStyledTool = (definition: any) => {
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4431
|
+
pi.registerToolRenderer((name, next) => {
|
|
4432
|
+
const downstream = next();
|
|
4433
|
+
if (name !== definition.name) return downstream;
|
|
4434
|
+
// A same-named extension/SDK tool owns its own rendering and parameters.
|
|
4435
|
+
// In particular, do not apply built-in summaries to an unrelated schema.
|
|
4436
|
+
const source = pi.getAllTools().find((tool) => tool.name === name)?.sourceInfo;
|
|
4437
|
+
if (source && source.path !== `builtin:${name}`) {
|
|
4438
|
+
return downstream ? { ...downstream, [THIRD_PARTY_TOOL]: true } : downstream;
|
|
4439
|
+
}
|
|
4440
|
+
const native = nativeDefinitions[name];
|
|
4441
|
+
const select = (kind: "renderCall" | "renderResult", args: any[]) => {
|
|
4442
|
+
const context = args.at(-1);
|
|
4443
|
+
const useNative = renderMode === "full" || renderMode === "compact" && context.expanded;
|
|
4444
|
+
const namespace = useNative ? "nativeFull" : "pretty";
|
|
4445
|
+
const state = context.state[namespace] ??= { state: {} };
|
|
4446
|
+
const scoped = { ...context, state: state.state, lastComponent: state[kind] };
|
|
4447
|
+
const renderer = useNative ? downstream?.[kind] ?? native[kind] : definition[kind];
|
|
4448
|
+
const component = renderer?.(...args.slice(0, -1), scoped);
|
|
4449
|
+
state[kind] = component;
|
|
4450
|
+
return component;
|
|
4451
|
+
};
|
|
4452
|
+
return { ...downstream, renderShell: "self",
|
|
4453
|
+
renderCall: (...args: any[]) => select("renderCall", args),
|
|
4454
|
+
renderResult: (...args: any[]) => select("renderResult", args),
|
|
4455
|
+
};
|
|
4351
4456
|
});
|
|
4352
4457
|
};
|
|
4353
4458
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foreground colors for Markdown strong and emphasis in the assistant's answer.
|
|
3
|
+
*
|
|
4
|
+
* Pi has no strong or emphasis color token, so these reuse existing ones. Each is
|
|
5
|
+
* read from the active theme on every render, so dark, light, custom themes and
|
|
6
|
+
* `/theme` all apply. `syntaxType` looks like an option for emphasis, but it equals
|
|
7
|
+
* `mdCode` in Pi's built-in themes, which would make emphasis look like inline code.
|
|
8
|
+
*/
|
|
9
|
+
export const MARKDOWN_EMPHASIS_COLORS = {
|
|
10
|
+
strong: "syntaxNumber",
|
|
11
|
+
em: "warning",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
type EmphasisType = keyof typeof MARKDOWN_EMPHASIS_COLORS;
|
|
15
|
+
type EmphasisColor = (typeof MARKDOWN_EMPHASIS_COLORS)[EmphasisType];
|
|
16
|
+
type ChildOwner = EmphasisType | "link";
|
|
17
|
+
|
|
18
|
+
export interface EmphasisTheme {
|
|
19
|
+
fg(color: EmphasisColor, text: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Pi's inline style context: how text is styled, and what restores it after a nested span closes. */
|
|
23
|
+
export interface InlineStyleContext {
|
|
24
|
+
applyText: (text: string) => string;
|
|
25
|
+
stylePrefix: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InlineToken {
|
|
29
|
+
type: string;
|
|
30
|
+
tokens?: InlineToken[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SENTINEL = "\0";
|
|
34
|
+
|
|
35
|
+
const isChildOwner = (type: string): type is ChildOwner =>
|
|
36
|
+
type === "strong" || type === "em" || type === "link";
|
|
37
|
+
|
|
38
|
+
// Pi passes a token's children to renderInlineTokens with the parent's context and
|
|
39
|
+
// does not say who the parent was. Keyed by array, so Pi's cached tokens stay unmodified.
|
|
40
|
+
const childOwners = new WeakMap<readonly InlineToken[], ChildOwner>();
|
|
41
|
+
const nativeContexts = new WeakSet<InlineStyleContext>();
|
|
42
|
+
const surroundingContexts = new WeakMap<InlineStyleContext, InlineStyleContext>();
|
|
43
|
+
|
|
44
|
+
/** Marks a context whose text keeps its own foreground, such as a heading title. */
|
|
45
|
+
export function withNativeEmphasis(context: InlineStyleContext): InlineStyleContext {
|
|
46
|
+
nativeContexts.add(context);
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function emphasisContext(
|
|
51
|
+
surrounding: InlineStyleContext,
|
|
52
|
+
color: EmphasisColor,
|
|
53
|
+
theme: EmphasisTheme,
|
|
54
|
+
): InlineStyleContext {
|
|
55
|
+
const context: InlineStyleContext = {
|
|
56
|
+
// The inner color wins, so a surrounding default style cannot hide the emphasis color.
|
|
57
|
+
applyText: (text) => surrounding.applyText(theme.fg(color, text)),
|
|
58
|
+
// A nested span's foreground close falls back to the terminal default, so
|
|
59
|
+
// Pi's stylePrefix has to reopen the emphasis color for the text after it.
|
|
60
|
+
stylePrefix: surrounding.stylePrefix + theme.fg(color, SENTINEL).split(SENTINEL)[0],
|
|
61
|
+
};
|
|
62
|
+
surroundingContexts.set(context, surrounding);
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the context Pi should use for these tokens. Only the context changes:
|
|
68
|
+
* Pi's own recursion still emits every flag and every nested span.
|
|
69
|
+
*
|
|
70
|
+
* The innermost strong or emphasis sets the foreground. A link label uses the
|
|
71
|
+
* context outside all emphasis, so it keeps Pi's link color, and emphasis inside
|
|
72
|
+
* it stays flags-only for the same reason. Inline code is untouched by contexts.
|
|
73
|
+
*/
|
|
74
|
+
export function themedInlineContext(
|
|
75
|
+
tokens: readonly InlineToken[],
|
|
76
|
+
context: InlineStyleContext,
|
|
77
|
+
theme: EmphasisTheme,
|
|
78
|
+
): InlineStyleContext {
|
|
79
|
+
const owner = childOwners.get(tokens);
|
|
80
|
+
for (const token of tokens) {
|
|
81
|
+
if (token.tokens && isChildOwner(token.type)) childOwners.set(token.tokens, token.type);
|
|
82
|
+
}
|
|
83
|
+
if (!owner || nativeContexts.has(context)) return context;
|
|
84
|
+
const surrounding = surroundingContexts.get(context) ?? context;
|
|
85
|
+
if (owner === "link") return withNativeEmphasis({ ...surrounding });
|
|
86
|
+
return emphasisContext(surrounding, MARKDOWN_EMPHASIS_COLORS[owner], theme);
|
|
87
|
+
}
|