* Studio: keep the executed Python script visible in chat, with download + viewport-gated highlight Always show the executed Python script under the tool card (not only inside the collapsible run/output section, which is unmounted from history), with Copy and a client-side .py Download button, so the script stays visible on reopen (#7165). The script is rendered eagerly, but shiki syntax-highlighting only runs once the block scrolls near the viewport (IntersectionObserver, 200px margin); until then a plain monospace placeholder shows the same source with matching padding, so there is no layout jump. This bounds highlighting to the cards actually on screen instead of tokenizing every script up front. Measured shiki cost is ~8 ms per typical 2 KB script, so eager highlighting of a long agentic transcript (20-50+ Python calls) would add ~170-420 ms of main-thread work on load; viewport-gating keeps it to the few visible cards (~15-35 ms) regardless of transcript length. Falls back to immediate highlight when IntersectionObserver is unavailable (SSR / tests). * Use a div for the pre-highlight placeholder so container [&_pre]:!p-0 doesn't strip its p-3 The placeholder shares the highlighted block's p-3 padding to avoid a layout jump, but as a <pre> it was caught by the container's [&_pre]:!p-0 !important rule and rendered with no padding, so the script shifted by p-3 when shiki swapped in. A plain div keeps the padding. * Match placeholder wrapping to the highlighted pre (whitespace-pre, not pre-wrap) The placeholder wrapped long lines while the highlighted Streamdown <pre> keeps them on one line and scrolls in the container's overflow-auto, so a script with a long line changed height when shiki swapped in. Use whitespace-pre so the placeholder scrolls the same way and the height stays stable. --------- Co-authored-by: danielhanchen <unslothai@gmail.com>
This commit is contained in:
parent
d49d23ffab
commit
beddfc963e
1 changed files with 53 additions and 12 deletions
|
|
@ -118,22 +118,63 @@ function DownloadBtn({ code, name = "script.py" }: { code: string; name?: string
|
|||
);
|
||||
}
|
||||
|
||||
/** Syntax-highlighted code via Streamdown + shiki; inherits parent container. */
|
||||
/** Syntax-highlighted code via Streamdown + shiki; inherits parent container.
|
||||
* The script is always in the DOM (a plain monospace placeholder), but shiki
|
||||
* only tokenizes once the block scrolls near the viewport, so a long transcript
|
||||
* with many scripts doesn't highlight every one up front. Falls back to
|
||||
* immediate highlight when IntersectionObserver is unavailable (SSR / tests). */
|
||||
function HighlightedCode({ code: source, language }: { code: string; language: string }) {
|
||||
const display = useMemo(() => truncate(source), [source]);
|
||||
const markdown = useMemo(
|
||||
() => `\`\`\`${language}\n${truncate(source)}\n\`\`\``,
|
||||
[source, language],
|
||||
() => `\`\`\`${language}\n${display}\n\`\`\``,
|
||||
[display, language],
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [highlight, setHighlight] = useState(
|
||||
() => typeof IntersectionObserver === "undefined",
|
||||
);
|
||||
useEffect(() => {
|
||||
if (highlight) return;
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
setHighlight(true);
|
||||
io.disconnect();
|
||||
}
|
||||
},
|
||||
// Highlight just before the block enters view so it's colorized by the
|
||||
// time the user reaches it, without tokenizing off-screen scripts.
|
||||
{ rootMargin: "200px" },
|
||||
);
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, [highlight]);
|
||||
return (
|
||||
<div className="max-h-48 overflow-auto text-xs [&_pre]:!m-0 [&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!text-xs [&_[data-streamdown=code-block]]:!my-0 [&_[data-streamdown=code-block]]:!p-3 [&_[data-streamdown=code-block]]:!border-0">
|
||||
<Streamdown
|
||||
mode="static"
|
||||
plugins={{ code: codePlugin }}
|
||||
controls={{ code: false }}
|
||||
shikiTheme={SHIKI_THEME}
|
||||
>
|
||||
{markdown}
|
||||
</Streamdown>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="max-h-48 overflow-auto text-xs [&_pre]:!m-0 [&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!text-xs [&_[data-streamdown=code-block]]:!my-0 [&_[data-streamdown=code-block]]:!p-3 [&_[data-streamdown=code-block]]:!border-0"
|
||||
>
|
||||
{highlight ? (
|
||||
<Streamdown
|
||||
mode="static"
|
||||
plugins={{ code: codePlugin }}
|
||||
controls={{ code: false }}
|
||||
shikiTheme={SHIKI_THEME}
|
||||
>
|
||||
{markdown}
|
||||
</Streamdown>
|
||||
) : (
|
||||
// A div, not a <pre>: the container's [&_pre]:!p-0 would override a
|
||||
// <pre>'s padding and shift the content by p-3 when shiki swaps in. Keep
|
||||
// the same p-3, and whitespace-pre (not pre-wrap) so long lines scroll in
|
||||
// the container's overflow-auto exactly like the highlighted <pre>, rather
|
||||
// than wrapping taller and then collapsing when shiki swaps in.
|
||||
<div className="whitespace-pre p-3 font-mono text-xs text-muted-foreground">
|
||||
{display}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue