1import {
2 commandHighlightLanguage,
3 withVisibleControlCharacters,
4} from "@t3tools/client-runtime/work-log/command-label";
5import { Suspense, use, useMemo } from "react";
6
7import { useTheme } from "../../hooks/useTheme";
8import { RenderErrorBoundary } from "../RenderErrorBoundary";
9import { HighlightedTokens } from "./HighlightedTokens";
10
11// Shell words wrap as a unit, so `--exclude` or a quoted string is not split
12// at a hyphen; a word longer than the line still breaks anywhere.
13const WORD_CLASS_NAME = "inline-block max-w-full [overflow-wrap:anywhere]";
14
15// The shell parser loads with the first expanded command, not with the timeline.
16let embeddedScriptsModule: Promise<typeof import("../../lib/embeddedScripts")> | undefined;
17function loadEmbeddedScripts() {
18 embeddedScriptsModule ??= import("../../lib/embeddedScripts");
19 return embeddedScriptsModule;
20}
21
22/** Same layout as the highlighted version, so the grammar arriving never reflows the block. */
23function PlainWords({ code }: { code: string }) {
24 // split with a capture group alternates words (even) and whitespace (odd).
25 return code.split(/(\s+)/u).map((part, index) =>
26 index % 2 === 1 || part === "" ? (
27 part
28 ) : (
29 // Positions are stable for a given string, which is all this renders.
30 // oxlint-disable-next-line react/no-array-index-key
31 <span key={index} className={WORD_CLASS_NAME}>
32 {part}
33 </span>
34 ),
35 );
36}
37
38function HighlightedCommand({ code, theme }: { code: string; theme: "light" | "dark" }) {
39 const { embeddedScripts } = use(loadEmbeddedScripts());
40 const language = commandHighlightLanguage(code);
41 // Only shell syntax nests scripts; PowerShell's own grammar colors its strings.
42 const embedded = useMemo(
43 () => (language === "shellscript" ? embeddedScripts(code) : []),
44 [code, embeddedScripts, language],
45 );
46 return (
47 <HighlightedTokens
48 code={code}
49 language={language}
50 embedded={embedded}
51 theme={theme}
52 wordClassName={WORD_CLASS_NAME}
53 />
54 );
55}
56
57/**
58 * The command a command_execution item ran, syntax highlighted. Scripts inside
59 * it, such as a `bash -lc` script or a Python heredoc, get their own grammar.
60 */
61export function ShellCommandBlock({ command }: { command: string }) {
62 const { resolvedTheme } = useTheme();
63 const code = withVisibleControlCharacters(command.trim());
64 if (!code) return null;
65 const plain = <PlainWords code={code} />;
66 return (
67 // The tool body sets the monospace, pre-wrapped text this sits in.
68 <div className="text-foreground/85">
69 <RenderErrorBoundary fallback={plain} resetKeys={[code]}>
70 <Suspense fallback={plain}>
71 <HighlightedCommand code={code} theme={resolvedTheme} />
72 </Suspense>
73 </RenderErrorBoundary>
74 </div>
75 );
76}