| 1 | import { useLayoutEffect, useMemo, useState, type CSSProperties } from "react"; |
| 2 | import { createRoot } from "react-dom/client"; |
| 3 | import { ChatPaneRegion, type ChatPaneTranscriptInput } from "../src/app-shell/ChatPaneRegion"; |
| 4 | import { DockLauncher } from "../src/components/DockLauncher"; |
| 5 | import { initialState, type Item } from "../src/lib/useController"; |
| 6 | import { applyConversationWidth } from "../src/lib/conversationWidth"; |
| 7 | import { LocaleProvider, useT } from "../src/lib/i18n"; |
| 8 | import { installDesktopHostStub } from "../src/__tests__/desktopHostStub"; |
| 9 | import "../src/styles.css"; |
| 10 | |
| 11 | type Options = { |
| 12 | layout: "workbench"; |
| 13 | width: "standard" | "full"; |
| 14 | sidebar: boolean; |
| 15 | dock: boolean; |
| 16 | launcher: boolean; |
| 17 | long: boolean; |
| 18 | turns: number; |
| 19 | text: string | null; |
| 20 | streaming: boolean; |
| 21 | shell: boolean; |
| 22 | }; |
| 23 | declare global { |
| 24 | interface Window { |
| 25 | transcriptLayoutFixture: { |
| 26 | configure: (next: Partial<Options>) => void; |
| 27 | revision: number; |
| 28 | copied: string[]; |
| 29 | command: string; |
| 30 | }; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | // Only the backend boundary is stubbed: layout, launcher, projection, windowing |
| 35 | // and message/code rendering below are the production components and stylesheet. |
| 36 | installDesktopHostStub({ |
| 37 | ReportFrontendDiagnostic: async () => {}, |
| 38 | GitBranches: async () => [], |
| 39 | WorkspaceChanges: async () => [], |
| 40 | ToolResultForTab: async () => null, |
| 41 | }); |
| 42 | const noAction = () => {}; |
| 43 | const copied: string[] = []; |
| 44 | const command = "printf '%s\\n' \"C:\\中文\\file.txt\"\r\n echo " + "long-command-".repeat(120) + "END_OF_COMMAND"; |
| 45 | Object.defineProperty(navigator, "clipboard", { configurable: true, value: { |
| 46 | writeText: async (text: string) => { copied.push(text); }, |
| 47 | } }); |
| 48 | const commands = { |
| 49 | onPrompt: noAction, onFork: undefined, onDeliveryContinue: undefined, onAcceptDelivery: undefined, |
| 50 | onOpenChanges: undefined, onOpenVerification: undefined, onEditPrompt: undefined, |
| 51 | onRewind: undefined, onLoadOlderHistory: undefined, onLoadNewerHistory: undefined, |
| 52 | onSurfacePaintReady: undefined, |
| 53 | }; |
| 54 | function Fixture() { |
| 55 | const t = useT(); |
| 56 | const [options, setOptions] = useState<Options>({ |
| 57 | layout: "workbench", width: "full", sidebar: true, dock: false, launcher: false, long: true, turns: 2, text: null, streaming: false, shell: false, |
| 58 | }); |
| 59 | const [revision, setRevision] = useState(0); |
| 60 | const items = useMemo(() => Array.from({ length: options.turns }, (_, index): Item[] => [ |
| 61 | { kind: "user", id: "user-" + index, text: "USER MESSAGE MUST REMAIN VISIBLE " + index }, |
| 62 | options.shell |
| 63 | ? { kind: "tool", id: "tool-" + index, name: "bash", status: "done", readOnly: false, |
| 64 | args: JSON.stringify({ command }), output: "OUTPUT_STAYS_VISIBLE\n" + "abcdefghij".repeat(60) } |
| 65 | : { kind: "assistant", id: "answer-" + index, text: options.text ?? ("\`\`\`text\n" + (options.long && (options.turns < 100 || index === 10) ? "abcdefghij".repeat(60) : "short") + "\n\`\`\`"), reasoning: "", streaming: options.streaming }, |
| 66 | ]).flat(), [options.long, options.turns, options.text, options.streaming, options.shell]); |
| 67 | useLayoutEffect(() => { |
| 68 | document.documentElement.dataset.themeStyle = "graphite"; |
| 69 | document.documentElement.dataset.theme = "light"; |
| 70 | document.documentElement.dataset.platform = "windows"; |
| 71 | applyConversationWidth(options.width); |
| 72 | window.transcriptLayoutFixture = { |
| 73 | configure: next => { setOptions(current => ({ ...current, ...next })); setRevision(value => value + 1); }, |
| 74 | revision, |
| 75 | copied, |
| 76 | command, |
| 77 | }; |
| 78 | }, [options.width, revision]); |
| 79 | const transcript: ChatPaneTranscriptInput = { |
| 80 | state: { ...initialState, items, running: options.streaming }, items, tabId: "layout-fixture", geometrySessionKey: "layout-fixture", |
| 81 | footerHeight: 100, invocationMetadata: undefined, surfaceCommitToken: undefined, |
| 82 | liveStore: undefined, transcriptHydrating: false, navigationDataReady: true, readOnly: false, |
| 83 | controllerReady: true, hydratePlaceholderActive: false, clearContextPending: false, |
| 84 | availability: { kind: "ready", source: "history" }, |
| 85 | rewind: { stateActive: false, committing: false }, |
| 86 | }; |
| 87 | return <div className={"app app--windows app--windows-frameless app--" + options.layout} data-fixture-revision={revision}> |
| 88 | <div className={`layout${options.sidebar ? "" : " layout--sidebar-collapsed"}${options.dock ? " layout--workspace-open" : ""}`} |
| 89 | style={{ "--sidebar-expanded-width": "300px", "--workspace-width": "300px" } as CSSProperties}> |
| 90 | <header className="topicbar">Transcript layout fixture</header> |
| 91 | <aside className="sidebar">Sidebar</aside> |
| 92 | <div className="chat-pane"> |
| 93 | <ChatPaneRegion transitioning={false} t={t} imDetail={null} remote={undefined} commands={commands} |
| 94 | transcript={transcript} onRetryHistory={async () => {}} |
| 95 | launcher={options.launcher ? <DockLauncher tabId="layout-fixture" scopeKey="layout-fixture" |
| 96 | workspaceRoot="" visible onSelect={noAction} overlay={options.dock} /> : undefined} /> |
| 97 | <footer className="footer"><div className="composer-wrap"><textarea aria-label="Draft" defaultValue="Draft remains inside the chat pane" /></div></footer> |
| 98 | </div> |
| 99 | {options.dock && <aside className="workbench-dock" style={{ gridColumn: 3, gridRow: 2 }}>Workspace</aside>} |
| 100 | </div> |
| 101 | </div>; |
| 102 | } |
| 103 | createRoot(document.getElementById("root")!).render(<LocaleProvider><Fixture /></LocaleProvider>); |
| 104 |