@av-pi-studio/web-client 0.0.66 → 0.0.67
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/dist/.tsbuildinfo +1 -1
- package/dist/web/assets/{BinaryFallbackViewer-CID9YLTN.js → BinaryFallbackViewer-CcuvrutG.js} +2 -2
- package/dist/web/assets/{BinaryFallbackViewer-CID9YLTN.js.map → BinaryFallbackViewer-CcuvrutG.js.map} +1 -1
- package/dist/web/assets/ChatPanel-DgCKLvEc.js +3 -0
- package/dist/web/assets/ChatPanel-DgCKLvEc.js.map +1 -0
- package/dist/web/assets/{CodeView-5z34CDuP.js → CodeView-CXE9vlF1.js} +2 -2
- package/dist/web/assets/{CodeView-5z34CDuP.js.map → CodeView-CXE9vlF1.js.map} +1 -1
- package/dist/web/assets/{FilePanel-Ck53TDKW.js → FilePanel-jN6FYiGo.js} +2 -2
- package/dist/web/assets/{FilePanel-Ck53TDKW.js.map → FilePanel-jN6FYiGo.js.map} +1 -1
- package/dist/web/assets/{ImageViewer-Cd0XzsH0.js → ImageViewer-DdvsjbtG.js} +2 -2
- package/dist/web/assets/{ImageViewer-Cd0XzsH0.js.map → ImageViewer-DdvsjbtG.js.map} +1 -1
- package/dist/web/assets/{MarkdownFileViewer-C2QKqSk4.js → MarkdownFileViewer-BNG2in_C.js} +2 -2
- package/dist/web/assets/{MarkdownFileViewer-C2QKqSk4.js.map → MarkdownFileViewer-BNG2in_C.js.map} +1 -1
- package/dist/web/assets/{MoleculeViewerPanel-v4ED-TPp.js → MoleculeViewerPanel-CQQ9CIsD.js} +2 -2
- package/dist/web/assets/{MoleculeViewerPanel-v4ED-TPp.js.map → MoleculeViewerPanel-CQQ9CIsD.js.map} +1 -1
- package/dist/web/assets/{TerminalPanel-CrhMXlZQ.js → TerminalPanel-zJX45luf.js} +2 -2
- package/dist/web/assets/{TerminalPanel-CrhMXlZQ.js.map → TerminalPanel-zJX45luf.js.map} +1 -1
- package/dist/web/assets/{TextViewer-DXqtZond.js → TextViewer-DLce2RZp.js} +2 -2
- package/dist/web/assets/{TextViewer-DXqtZond.js.map → TextViewer-DLce2RZp.js.map} +1 -1
- package/dist/web/assets/{VideoViewer-tI0bhkFe.js → VideoViewer-I4xBhKGc.js} +2 -2
- package/dist/web/assets/{VideoViewer-tI0bhkFe.js.map → VideoViewer-I4xBhKGc.js.map} +1 -1
- package/dist/web/assets/index-KQwi4ZXj.js +18 -0
- package/dist/web/assets/index-KQwi4ZXj.js.map +1 -0
- package/dist/web/assets/markdown-CfR91Pqp.js +2 -0
- package/dist/web/assets/markdown-CfR91Pqp.js.map +1 -0
- package/dist/web/assets/{use-file-download-DAb889OO.js → use-file-download-Dv5HRkkB.js} +2 -2
- package/dist/web/assets/{use-file-download-DAb889OO.js.map → use-file-download-Dv5HRkkB.js.map} +1 -1
- package/dist/web/assets/{use-file-watch-cbUV4OS4.js → use-file-watch-DTOtoP8W.js} +2 -2
- package/dist/web/assets/{use-file-watch-cbUV4OS4.js.map → use-file-watch-DTOtoP8W.js.map} +1 -1
- package/dist/web/assets/{vendor-molviewer-CARaM-YQ.js → vendor-molviewer-DE5Dt5UM.js} +137 -137
- package/dist/web/assets/{vendor-molviewer-CARaM-YQ.js.map → vendor-molviewer-DE5Dt5UM.js.map} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +5 -5
- package/dist/web/assets/ChatPanel-BlQXKvbg.js +0 -3
- package/dist/web/assets/ChatPanel-BlQXKvbg.js.map +0 -1
- package/dist/web/assets/index-DCTqsrDx.js +0 -18
- package/dist/web/assets/index-DCTqsrDx.js.map +0 -1
- package/dist/web/assets/markdown-YhzfNBqU.js +0 -2
- package/dist/web/assets/markdown-YhzfNBqU.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatPanel-DgCKLvEc.js","sources":["../../../src/timeline/tool-mapping.ts","../../../src/lib/connection/files-changed.ts","../../../src/hooks/agent-stream-events.ts","../../../src/hooks/use-agent-stream.ts","../../../src/features/chat/rows/AssistantRow.tsx","../../../src/features/chat/rows/ReasoningRow.tsx","../../../src/features/chat/rows/ToolCard.tsx","../../../src/features/chat/rows/UserRow.tsx","../../../src/features/chat/rows/ErrorRow.tsx","../../../src/features/chat/rows/SystemRow.tsx","../../../src/features/chat/Timeline.tsx","../../../src/hooks/use-agent-commands.ts","../../../src/features/chat/Attachments.tsx","../../../src/features/chat/CommandMenu.tsx","../../../src/features/chat/slash-commands.ts","../../../src/features/chat/Composer.tsx","../../../src/features/chat/ChatPanel.tsx"],"sourcesContent":["/**\n * tool.kind → display label + icon. Scoped to the protocol's `ToolCallDetail` union\n * (shell/read/edit/write/search/fetch/task) — POC_TO_APP_PLAN_UI.md §3 `timeline/tool-mapping.ts`.\n */\n\nimport type { ToolCallDetail } from \"@av-pi-studio/protocol\";\nimport {\n Terminal,\n Eye,\n Pencil,\n FilePlus,\n Search,\n Globe,\n ListTree,\n type LucideIcon,\n} from \"lucide-react\";\nimport { toolDetailText } from \"./row-model.js\";\n\nconst ICON_BY_KIND: Record<ToolCallDetail[\"kind\"], LucideIcon> = {\n shell: Terminal,\n read: Eye,\n edit: Pencil,\n write: FilePlus,\n search: Search,\n fetch: Globe,\n task: ListTree,\n};\n\nexport function toolIcon(tool: ToolCallDetail): LucideIcon {\n return ICON_BY_KIND[tool.kind] ?? Terminal;\n}\n\n/** Human label for a tool card header, e.g. \"shell\", \"edit\". */\nexport function toolLabel(tool: ToolCallDetail): string {\n return tool.kind;\n}\n\n/** One-line detail summary for a tool card (POC: `t.command || t.path || t.query || …`). */\nexport function toolSummary(tool: ToolCallDetail): string {\n return toolDetailText(tool);\n}\n\n/** File path a tool touched, if any — used to target `filesChanged` invalidation (§4.5). */\nexport function toolFilePath(tool: ToolCallDetail): string | null {\n switch (tool.kind) {\n case \"read\":\n case \"write\":\n case \"edit\":\n return tool.path ?? null;\n default:\n return null;\n }\n}\n\n/** True for tool kinds whose completion should invalidate open file/diff/explorer/git queries. */\nexport function toolMutatesFiles(tool: ToolCallDetail): boolean {\n return tool.kind === \"write\" || tool.kind === \"edit\" || tool.kind === \"shell\";\n}\n\n/**\n * Expandable detail body for a tool card, as an ordered list of sections. `diff` renders as a\n * `<DiffView>`; `code` renders in a monospace block. Order: input detail (command/path/query/…)\n * first, then tool output (stdout, file content, etc.) if present. Empty array = header-only card\n * with nothing worth expanding.\n */\nexport type ToolBodySection = { kind: \"diff\"; patch: string } | { kind: \"code\"; text: string };\n\nexport function toolBody(tool: ToolCallDetail): ToolBodySection[] {\n const sections: ToolBodySection[] = [];\n if (tool.kind === \"edit\" && tool.diff) sections.push({ kind: \"diff\", patch: tool.diff });\n else {\n const text = toolDetailText(tool);\n if (text) sections.push({ kind: \"code\", text });\n }\n if (tool.output) sections.push({ kind: \"code\", text: tool.output });\n return sections;\n}\n\n/** Concise inline label shown in the collapsed header (basename for paths, first line for commands). */\nexport function toolInlineDetail(tool: ToolCallDetail): string {\n const text = toolDetailText(tool);\n if (!text) return \"\";\n switch (tool.kind) {\n case \"read\":\n case \"write\":\n case \"edit\": {\n const parts = text.split(\"/\");\n return parts[parts.length - 1] || text;\n }\n case \"shell\": {\n const firstLine = text.split(\"\\n\", 1)[0] ?? text;\n return firstLine;\n }\n default:\n return text;\n }\n}\n","/**\n * `filesChanged` signal — invalidates the file/diff/explorer query caches after a completed\n * write/edit/shell tool call (POC `scheduleFileRefresh`/`reloadOpenFileTabs`, chat.html\n * ~line 1070-1092, POC_TO_APP_PLAN_UI.md §4.3/§4.5/§5). Debounced ~500ms to coalesce a burst of\n * tool completions (POC used `setTimeout(...,600)`/`400`).\n *\n * As of task-006/008, the daemon ALSO pushes `file_changed` directly for any expanded directory\n * (`use-explorer-watch.ts`, a real `fs.watch`-backed daemon push) — covering non-agent filesystem\n * changes (an external editor, a shell command run outside the tool-call path, `git` in a\n * pi-studio terminal) that this debounced post-tool invalidation cannot see at all, since it only\n * fires from the timeline reducer's own `tool_call` completions. This module remains as the\n * belt-and-braces path for two things directory watching does NOT cover: it also invalidates\n * `[\"file\"]` *content* queries (an open text tab's read cache), and it covers a tool completion\n * even when the affected directory happens not to be expanded in the tree right now. The daemon's\n * 150ms coalescing (task-006) sits well under this module's 500ms debounce, so the two paths\n * cannot thrash each other.\n *\n * The daemon does NOT push a `checkout_status_update` automatically after a git-affecting tool\n * completion, so this module also fires an explicit `checkout_refresh_request` for the active\n * cwd — `ChangesPanel`/`FileExplorer` then pick up the resulting push/refetch reactively, since\n * they're already subscribed (`use-checkout-status.ts`) or query-cache-driven (`use-explorer.ts`).\n *\n * NOTE (follow-up, cross-slice): this module only *exports* the invalidation function. The actual\n * call site — wiring this into the timeline reducer's `tool_call` handling on `status:\"completed\"`\n * for `toolMutatesFiles(tool) === true` — belongs to the chat/session-store slice (built in\n * parallel) or the top-level app wiring; do not import/call it from `timeline/reducer.ts` here,\n * that file is a shared contract file owned across slices.\n */\n\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\nconst DEBOUNCE_MS = 500;\nlet pendingTimer: ReturnType<typeof setTimeout> | undefined;\n\n/**\n * Invalidate file/diff/explorer TanStack Query caches and refresh git status for `cwd`.\n * Debounced: repeated calls within `DEBOUNCE_MS` coalesce into a single invalidation pass.\n *\n * `toolFilePath` is accepted for future path-scoped invalidation (POC `reloadOpenFileTabs`\n * matched by path suffix) but the current pass invalidates the whole `file`/`explorer` query\n * families — TanStack Query dedupes refetches for inactive queries, so this is cheap and correct\n * even when the path is unknown.\n */\nexport function invalidateAfterToolCompletion(\n queryClient: QueryClient,\n client: PiStudioClient | null,\n cwd: string | null,\n _toolFilePath: string | null,\n): void {\n clearTimeout(pendingTimer);\n pendingTimer = setTimeout(() => {\n pendingTimer = undefined;\n void queryClient.invalidateQueries({ queryKey: [\"file\"] });\n void queryClient.invalidateQueries({ queryKey: [\"explorer\"] });\n if (client && cwd) {\n void client.connection.request(\"checkout_refresh_request\", { cwd }).catch(() => {});\n }\n }, DEBOUNCE_MS);\n}\n","/**\n * Shared `AgentStreamEvent` → store-update logic, consumed by the live subscription\n * (`use-agent-stream.ts`), which attaches the instant a session's `agentId` is bound —\n * including a freshly materialized draft, before its first turn can possibly start streaming\n * (`Composer.tsx`'s file header) — so there is no separate raw-broadcast/first-turn-gate path\n * here anymore.\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\nimport type { AgentStreamEvent } from \"@av-pi-studio/protocol\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { toolMutatesFiles, toolFilePath } from \"@pi-studio-ui/timeline/tool-mapping.js\";\nimport { invalidateAfterToolCompletion } from \"@pi-studio-ui/lib/connection/files-changed.js\";\n\nexport interface ApplyAgentStreamEventArgs {\n sessionId: string;\n event: AgentStreamEvent;\n client: PiStudioClient;\n queryClient: QueryClient;\n}\n\nexport function applyAgentStreamEvent({\n sessionId,\n event,\n client,\n queryClient,\n}: ApplyAgentStreamEventArgs): void {\n const sessionStore = useSessionStore.getState();\n sessionStore.applyStreamEvent(sessionId, event);\n switch (event.kind) {\n case \"turn_started\":\n sessionStore.setStatus(sessionId, \"running\");\n break;\n case \"turn_completed\":\n sessionStore.setStatus(sessionId, \"idle\");\n break;\n case \"turn_failed\":\n sessionStore.setStatus(sessionId, \"error\");\n break;\n case \"turn_canceled\":\n sessionStore.setStatus(sessionId, \"idle\");\n break;\n case \"tool_call\": {\n if (event.status === \"completed\" && toolMutatesFiles(event.tool)) {\n const session = sessionStore.sessions[sessionId];\n const cwd = session?.cwd || null;\n invalidateAfterToolCompletion(queryClient, client, cwd, toolFilePath(event.tool));\n }\n break;\n }\n default:\n break;\n }\n}\n","/**\n * Live timeline subscription for a chat session. Subscribes to\n * `client.agent(agentId).timeline.subscribe` whenever both the connection's `client` and the\n * session's `agentId` are available, routing every event through `applyAgentStreamEvent`\n * (POC_TO_APP_PLAN_UI.md §4.3). Also mirrors the POC's `handleAgentStream` turn-lifecycle →\n * `session.status` transitions so the Composer's Stop button and the session-list status dot\n * stay live.\n *\n * By design, a deferred draft's `agentId` is bound before any turn can start (at tab-open time\n * via `ensureMaterialized` in the Composer), so this subscription attaches before streaming\n * begins. There is no raw-broadcast first-turn workaround — every event is captured by the live\n * subscription, including the first turn.\n */\n\nimport { useEffect } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { applyAgentStreamEvent } from \"./agent-stream-events.js\";\n\nexport function useAgentStream(sessionId: string): void {\n const client = useConnectionStore((s) => s.client);\n const agentId = useSessionStore((s) => s.sessions[sessionId]?.agentId ?? null);\n const queryClient = useQueryClient();\n\n useEffect(() => {\n if (!client || !agentId) return;\n\n const unsubscribe = client.agent(agentId).timeline.subscribe((event) => {\n applyAgentStreamEvent({ sessionId, event, client, queryClient });\n });\n\n return unsubscribe;\n }, [client, agentId, sessionId, queryClient]);\n}\n","/**\n * Assistant message row (POC `.msg.assistant`, POC_TO_APP_PLAN_UI.md §4.3). While streaming,\n * renders plain text with a blinking cursor — re-parsing markdown on every token would be\n * wasteful (§6 Performance strategy). The reducer flips `streaming: false` as soon as the text\n * can no longer grow (Pi's `text_end` block-close marker, or the next tool call / turn boundary),\n * at which point it renders through `<Markdown>` — not at the end of the whole turn.\n */\n\nimport type { AssistantRow as AssistantRowModel } from \"@pi-studio-ui/timeline/row-model.js\";\nimport { Markdown } from \"@pi-studio-ui/timeline/markdown.js\";\nimport styles from \"./rows.module.css\";\n\nexport interface AssistantRowProps {\n row: AssistantRowModel;\n assetBase?: string | null;\n owningPaneId?: string | null;\n workspaceCwd?: string | null;\n}\n\nexport function AssistantRow({\n row,\n assetBase = null,\n owningPaneId = null,\n workspaceCwd = null,\n}: AssistantRowProps) {\n return (\n <div\n className={`${styles.row} ${styles.assistant}${row.streaming ? ` ${styles.streaming}` : \"\"}`}\n >\n <span className={styles.who}>assistant</span>\n {row.streaming ? (\n <span className={styles.streamingText}>\n {row.text}\n <span className={styles.cursor}>▍</span>\n </span>\n ) : (\n <Markdown\n text={row.text}\n assetBase={assetBase}\n owningPaneId={owningPaneId}\n workspaceCwd={workspaceCwd}\n />\n )}\n </div>\n );\n}\n","/**\n * Reasoning row — dimmer/italic styling, visually distinct from assistant replies (POC\n * `.msg.reasoning`, POC_TO_APP_PLAN_UI.md §4.3). Streams as plain text with a cursor, same\n * rationale as `AssistantRow`.\n */\n\nimport type { ReasoningRow as ReasoningRowModel } from \"@pi-studio-ui/timeline/row-model.js\";\nimport { Markdown } from \"@pi-studio-ui/timeline/markdown.js\";\nimport styles from \"./rows.module.css\";\n\nexport interface ReasoningRowProps {\n row: ReasoningRowModel;\n owningPaneId?: string | null;\n workspaceCwd?: string | null;\n}\n\nexport function ReasoningRow({ row, owningPaneId = null, workspaceCwd = null }: ReasoningRowProps) {\n return (\n <div className={`${styles.row} ${styles.reasoning}`}>\n <span className={styles.who}>thinking</span>\n {row.streaming ? (\n <span className={styles.streamingText}>\n {row.text}\n <span className={styles.cursor}>▍</span>\n </span>\n ) : (\n <Markdown text={row.text} owningPaneId={owningPaneId} workspaceCwd={workspaceCwd} />\n )}\n </div>\n );\n}\n","/**\n * Tool call card — icon + label + inline detail + status badge header, with an expandable body\n * (POC `.msg.tool`, POC_TO_APP_PLAN_UI.md §4.3). Collapsed by default; clicking the header reveals\n * the full detail as an ordered list of sections: input detail (`edit` diffs render through the\n * shared `<DiffView>`; shell command/path/query/url/task description render in a monospace block)\n * followed by tool output (stdout, file content, etc.), when present. Cards with no expandable\n * sections (`toolBody` → `[]`) show only the header. The virtualized timeline re-measures on\n * expand via `measureElement`, so height changes need no special handling here.\n */\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport { StatusBadge } from \"@pi-studio-ui/components/primitives/StatusBadge.js\";\nimport { DiffView } from \"@pi-studio-ui/features/files/DiffView.js\";\nimport {\n toolIcon,\n toolLabel,\n toolInlineDetail,\n toolBody,\n} from \"@pi-studio-ui/timeline/tool-mapping.js\";\nimport type { ToolRow } from \"@pi-studio-ui/timeline/row-model.js\";\nimport styles from \"./rows.module.css\";\n\nexport interface ToolCardProps {\n row: ToolRow;\n}\n\nconst BADGE_VARIANT = {\n running: \"muted\",\n completed: \"success\",\n error: \"error\",\n} as const;\n\nexport function ToolCard({ row }: ToolCardProps) {\n const [expanded, setExpanded] = useState(false);\n const Icon = toolIcon(row.tool);\n const inlineDetail = toolInlineDetail(row.tool);\n const body = toolBody(row.tool);\n const hasBody = body.length > 0;\n\n return (\n <div className={clsx(styles.row, styles.tool)}>\n <button\n type=\"button\"\n className={styles.toolHeader}\n onClick={() => hasBody && setExpanded((v) => !v)}\n aria-expanded={hasBody ? expanded : undefined}\n disabled={!hasBody}\n >\n {hasBody && (\n <ChevronRight\n size={12}\n className={clsx(styles.toolChevron, expanded && styles.toolChevronOpen)}\n />\n )}\n <span className={clsx(styles.toolIcon, styles[row.status])}>\n <Icon size={14} />\n </span>\n <span className={styles.toolLabel}>{toolLabel(row.tool)}</span>\n {inlineDetail && (\n <span className={styles.toolInlineDetail} title={inlineDetail}>\n {inlineDetail}\n </span>\n )}\n <StatusBadge\n className={styles.toolBadge}\n label={row.status}\n variant={BADGE_VARIANT[row.status]}\n />\n </button>\n {expanded && hasBody && (\n <div className={styles.toolBody}>\n {body.map((section, i) =>\n section.kind === \"diff\" ? (\n <DiffView key={i} patch={section.patch} />\n ) : (\n <pre key={i} className={styles.toolCode}>\n {section.text}\n </pre>\n ),\n )}\n </div>\n )}\n </div>\n );\n}\n","/**\n * User message row — plain text + optional image thumbnails (POC `.msg.user`,\n * POC_TO_APP_PLAN_UI.md §4.3/§4.4). Images render at up to 200×150, matching the POC's inline\n * `userBubble.appendChild(img)` styling. Clicking a thumbnail opens it full-size in a `Dialog`.\n *\n * `row.pending` (optimistic echo not yet confirmed by the server's `user_message` broadcast —\n * see Composer.tsx) dims the row via CSS; `row.failed` (the RPC rejected before confirmation\n * ever arrived) tints it toward the error color and appends a \"failed to send\" label instead.\n * `row.queued` (steered mid-turn, not yet handed to the LLM — cleared by a `queue_update` stream\n * event, see `timeline/reducer.ts`'s `onQueueUpdate`) shows a small \"queued\" pill next to the\n * sender label. All three are terminal/transient states set by the reducer — this component only\n * reflects them.\n */\n\nimport { useState } from \"react\";\nimport { Clock } from \"lucide-react\";\nimport { Dialog } from \"@pi-studio-ui/components/primitives/Dialog.js\";\nimport type { UserRow as UserRowModel } from \"@pi-studio-ui/timeline/row-model.js\";\nimport styles from \"./rows.module.css\";\n\nexport interface UserRowProps {\n row: UserRowModel;\n}\n\nexport function UserRow({ row }: UserRowProps) {\n const [openedSrc, setOpenedSrc] = useState<string | null>(null);\n\n const stateClass = row.failed\n ? styles.userFailed\n : row.pending\n ? styles.userPending\n : row.queued\n ? styles.userQueued\n : \"\";\n\n return (\n <div className={`${styles.row} ${styles.user}${stateClass ? ` ${stateClass}` : \"\"}`}>\n <span className={styles.who}>\n you{row.failed ? \" · failed to send\" : \"\"}\n {row.queued && !row.failed && (\n <span className={styles.queuedBadge}>\n <Clock size={9} />\n queued\n </span>\n )}\n </span>\n {row.text}\n {row.images && row.images.length > 0 && (\n <div className={styles.userImages}>\n {row.images.map((img, i) => {\n const src = `data:${img.mimeType ?? \"image/png\"};base64,${img.data ?? \"\"}`;\n return (\n <img\n key={i}\n className={styles.userImage}\n src={src}\n alt=\"\"\n role=\"button\"\n tabIndex={0}\n onClick={() => setOpenedSrc(src)}\n onKeyDown={(ev) => {\n if (ev.key === \"Enter\" || ev.key === \" \") setOpenedSrc(src);\n }}\n />\n );\n })}\n </div>\n )}\n <Dialog\n open={openedSrc !== null}\n onOpenChange={(open) => {\n if (!open) setOpenedSrc(null);\n }}\n title=\"Image\"\n width=\"auto\"\n bare\n className={styles.imageDialog}\n >\n {openedSrc && <img className={styles.imageDialogPreview} src={openedSrc} alt=\"\" />}\n </Dialog>\n </div>\n );\n}\n","/**\n * Error row — red-tinted block (POC `.msg.err`, POC_TO_APP_PLAN_UI.md §4.3), used for\n * `turn_failed`/`error` stream events.\n */\n\nimport type { ErrorRow as ErrorRowModel } from \"@pi-studio-ui/timeline/row-model.js\";\nimport styles from \"./rows.module.css\";\n\nexport interface ErrorRowProps {\n row: ErrorRowModel;\n}\n\nexport function ErrorRow({ row }: ErrorRowProps) {\n return (\n <div className={`${styles.row} ${styles.error}`}>\n <span className={styles.who}>error</span>\n {row.text}\n </div>\n );\n}\n","/**\n * System row — muted/italic styling (POC's `turn_canceled` \"(canceled)\" reasoning-styled bubble,\n * POC_TO_APP_PLAN_UI.md §4.3), used for canceled-turn markers and other system notices.\n */\n\nimport type { SystemRow as SystemRowModel } from \"@pi-studio-ui/timeline/row-model.js\";\nimport styles from \"./rows.module.css\";\n\nexport interface SystemRowProps {\n row: SystemRowModel;\n}\n\nexport function SystemRow({ row }: SystemRowProps) {\n return <div className={`${styles.row} ${styles.system}`}>{row.text}</div>;\n}\n","/**\n * Virtualized timeline viewport — TanStack Virtual over `session.timeline.rows` with\n * variable-size rows (POC `.chat-area`, POC_TO_APP_PLAN_UI.md §4.3/§6). Auto-scrolls to the\n * bottom on new rows unless the user has scrolled up more than 40px from the bottom; re-sticks\n * whenever the row count grows from a user's own send (POC's forced `scrollTop = scrollHeight`,\n * fixed to respect manual scroll-up). The mount-time \"grew\" tracking ref reverts itself on\n * cleanup — required for correctness under React StrictMode's dev-only double-invoke, which\n * otherwise swallows the necessary scroll-to-bottom on a freshly restored/opened session (see\n * the effect's own comment).\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { measureElement as measureElementDefault, useVirtualizer } from \"@tanstack/react-virtual\";\nimport type { SessionEntry } from \"@pi-studio-ui/stores/session-store.js\";\nimport { normalizeCwd } from \"@pi-studio-ui/features/sessions/workspace-grouping.js\";\nimport { useHomeDir } from \"@pi-studio-ui/hooks/use-home-dir.js\";\nimport type { TimelineRow } from \"@pi-studio-ui/timeline/row-model.js\";\nimport { AssistantRow } from \"./rows/AssistantRow.js\";\nimport { ReasoningRow } from \"./rows/ReasoningRow.js\";\nimport { ToolCard } from \"./rows/ToolCard.js\";\nimport { UserRow } from \"./rows/UserRow.js\";\nimport { ErrorRow } from \"./rows/ErrorRow.js\";\nimport { SystemRow } from \"./rows/SystemRow.js\";\nimport styles from \"./Timeline.module.css\";\n\nexport interface TimelineProps {\n session: SessionEntry;\n owningPaneId: string | null;\n workspaceCwd: string;\n}\n\nconst STICK_THRESHOLD_PX = 40;\n\nfunction renderRow(\n row: TimelineRow,\n assetBase: string | null,\n owningPaneId?: string | null,\n workspaceCwd?: string,\n) {\n switch (row.kind) {\n case \"user\":\n return <UserRow row={row} />;\n case \"assistant\":\n return (\n <AssistantRow\n row={row}\n assetBase={assetBase}\n owningPaneId={owningPaneId}\n workspaceCwd={workspaceCwd}\n />\n );\n case \"reasoning\":\n return <ReasoningRow row={row} owningPaneId={owningPaneId} workspaceCwd={workspaceCwd} />;\n case \"tool\":\n return <ToolCard row={row} />;\n case \"error\":\n return <ErrorRow row={row} />;\n case \"system\":\n return <SystemRow row={row} />;\n }\n}\n\nexport function Timeline({ session, owningPaneId, workspaceCwd }: TimelineProps) {\n const rows = session.timeline.rows;\n const running = session.status === \"running\";\n const homeDir = useHomeDir();\n const assetBase = normalizeCwd(session.cwd, homeDir);\n const scrollRef = useRef<HTMLDivElement>(null);\n const stickToBottomRef = useRef(true);\n // Init to 0 (not `rows.length`) so a freshly-mounted tab with existing history — a brand-new\n // chat tab switch or a session restored from disk — still counts as \"grew\" on its first effect\n // run and scrolls to the last message, instead of opening at the top.\n const prevRowCountRef = useRef(0);\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => 48,\n overscan: 8,\n getItemKey: (index) => rows[index]?.id ?? index,\n // `TabPanelHost` keeps inactive chat tabs mounted under `display:none` rather than\n // unmounting them (so switching tabs preserves scroll position — see its own doc comment).\n // But a `display:none` ancestor collapses every row to a 0×0 border box, and the default\n // `measureElement` would cache that bogus 0 height — shrinking `.inner`'s total height to\n // ~0 and permanently clamping the scroll container's `scrollTop` to 0 in the process, which\n // survives the tab becoming visible again (the browser never un-clamps a scroll offset just\n // because content grows back). Skip the measurement while hidden and keep whatever size is\n // already cached (or the `estimateSize` default on first mount) instead.\n measureElement: (element, entry, instance) => {\n if ((element as HTMLElement).offsetParent !== null)\n return measureElementDefault(element, entry, instance);\n const index = instance.indexFromElement(element);\n const key = instance.options.getItemKey(index);\n return instance.itemSizeCache.get(key) ?? instance.options.estimateSize(index);\n },\n });\n\n function handleScroll(): void {\n const el = scrollRef.current;\n if (!el) return;\n const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n stickToBottomRef.current = distanceFromBottom < STICK_THRESHOLD_PX;\n }\n\n useEffect(() => {\n const prevCount = prevRowCountRef.current;\n const grew = rows.length > prevCount;\n prevRowCountRef.current = rows.length;\n if (grew && stickToBottomRef.current) {\n virtualizer.scrollToIndex(rows.length - 1, { align: \"end\" });\n }\n // React StrictMode double-invokes this effect on mount (mount -> phantom cleanup -> mount,\n // same instance — see TerminalPanel.tsx's own StrictMode doc comment for the established\n // pattern here). `@tanstack/react-virtual`'s own scroll-element attachment effect DOES\n // correctly redo its setup across that phantom cycle and, in doing so, resets the DOM\n // `scrollTop` back to 0 on the second (real) attach — but without this cleanup, this ref's\n // `grew` flip from the first (phantom) invocation would make the second invocation a false\n // \"unchanged\" no-op, permanently losing the scroll-to-bottom to the library's reset and\n // leaving a freshly restored/opened session's timeline stuck at the top. Reverting the ref\n // on cleanup makes each real invocation see the true count it started from.\n return () => {\n prevRowCountRef.current = prevCount;\n };\n }, [rows.length, virtualizer]);\n\n if (rows.length === 0) {\n return (\n <div className={styles.viewport} ref={scrollRef}>\n {running ? (\n <div className={styles.working} role=\"status\" aria-live=\"polite\">\n <span className={styles.workingDots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n Agent is working…\n </div>\n ) : (\n <div className={styles.empty}>No messages yet — say something to start.</div>\n )}\n </div>\n );\n }\n\n return (\n <div className={styles.viewport} ref={scrollRef} onScroll={handleScroll}>\n <div className={styles.inner} style={{ height: virtualizer.getTotalSize() }}>\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const row = rows[virtualRow.index];\n if (!row) return null;\n return (\n <div\n key={virtualRow.key}\n ref={virtualizer.measureElement}\n data-index={virtualRow.index}\n className={styles.rowWrap}\n style={{ transform: `translateY(${virtualRow.start}px)` }}\n >\n {renderRow(row, assetBase, owningPaneId, workspaceCwd)}\n </div>\n );\n })}\n </div>\n {running && (\n <div className={styles.working} role=\"status\" aria-live=\"polite\">\n <span className={styles.workingDots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n Agent is working…\n </div>\n )}\n </div>\n );\n}\n","/**\n * `use-agent-commands` — TanStack Query wrapper over `agent_list_commands_request`\n * (`client.agent(id).listCommands()`, sprint-040 web-client half). Cached exactly like\n * `use-provider-models.ts`: read-through by `[\"agents\",\"commands\",sessionId]`, no `staleTime`/\n * `gcTime`/`retry` override of its own, so a subsequent open of the `/` menu — including the\n * auto-open that fires on every `/` keystroke — shows the cached rows immediately\n * (`isLoading: false`) while a background refetch keeps the list current, instead of re-showing\n * a loading spinner every time.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { AgentCommandDescriptor } from \"@av-pi-studio/protocol\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { ensureMaterialized } from \"@pi-studio-ui/stores/materialize.js\";\n\nexport function useAgentCommands(sessionId: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.agentCommands(sessionId),\n queryFn: async (): Promise<AgentCommandDescriptor[]> => {\n if (!client) throw new Error(\"not connected\");\n // Same materialization guard `Composer.submit()`'s send branch uses — a no-op once bound.\n const agentId = await ensureMaterialized(client, sessionId);\n const { commands } = await client.agent(agentId).listCommands();\n return commands;\n },\n enabled: Boolean(client) && Boolean(sessionId) && enabled,\n });\n}\n","/**\n * Attachment preview strip — presentational thumbnail row with remove buttons (POC\n * `renderAttachments`, POC_TO_APP_PLAN_UI.md §4.4). Pending-image state lives in `Composer`;\n * this component only renders `images` and forwards `onRemove(index)`.\n */\n\nimport styles from \"./Attachments.module.css\";\n\nexport interface PendingImage {\n mimeType: string;\n /** Base64 payload, no `data:...;base64,` prefix — sent to the daemon as-is. */\n data: string;\n /** Full data URL, for the thumbnail `<img src>`. */\n thumbUrl: string;\n}\n\nexport interface AttachmentsProps {\n images: PendingImage[];\n onRemove: (index: number) => void;\n}\n\nexport function Attachments({ images, onRemove }: AttachmentsProps) {\n if (images.length === 0) return null;\n\n return (\n <div className={styles.strip}>\n {images.map((img, i) => (\n <div key={i} className={styles.thumb}>\n <img src={img.thumbUrl} alt=\"\" />\n <button\n type=\"button\"\n className={styles.remove}\n title=\"Remove\"\n onClick={() => onRemove(i)}\n >\n ×\n </button>\n </div>\n ))}\n </div>\n );\n}\n\n/** `FileReader.readAsDataURL` wrapped as a promise (POC `addImageFile`). */\nexport function readImageFile(file: File): Promise<PendingImage> {\n const { promise, resolve, reject } = Promise.withResolvers<PendingImage>();\n const reader = new FileReader();\n reader.onload = () => {\n const result = reader.result;\n if (typeof result !== \"string\") {\n reject(new Error(\"Unexpected FileReader result\"));\n return;\n }\n const commaIndex = result.indexOf(\",\");\n const data = commaIndex >= 0 ? result.slice(commaIndex + 1) : result;\n resolve({ mimeType: file.type, data, thumbUrl: result });\n };\n reader.onerror = () => reject(reader.error ?? new Error(\"Failed to read file\"));\n reader.readAsDataURL(file);\n return promise;\n}\n","/**\n * CommandMenu — the `/` slash-command popup (web-client slash commands). Reuses `ModelMenu`'s\n * visual chrome (`ModelMenu.module.css`'s `.content`/`.list`/`.item`/`.label`/`.modelId`/`.state`/\n * `.stateError`) plus command-row-specific classes added to the same file\n * (`.commandContent`/`.commandItem`/`.commandItemHeader`/`.commandDescription` — a capped popup\n * width with the name and source badge on one line, description clamped to two lines below, full\n * text on hover via the native `title` attribute) — but this is a DIFFERENT component, not a\n * generalized `ModelMenu`: `ModelMenu` owns its `open`/`query` state privately and renders its own\n * search `<input>`, neither of which fits a menu whose open state and filter query are driven\n * entirely by the composer's own textarea.\n *\n * Rows are plain `<div role=\"option\">`, not `DropdownMenu.Item` — `Item` brings Radix roving\n * focus/typeahead, which would fight the textarea for keyboard focus. `.itemActive` (keyboard\n * selection — what Enter/Tab applies) comes purely from `highlightedIndex`, driven by the\n * composer's own `ArrowUp`/`ArrowDown` handling; mouse hover is deliberately a SEPARATE, native\n * CSS `:hover` (see `.commandItem:hover` in `ModelMenu.module.css`) rather than being synced into\n * the same state via `onMouseEnter` — that would add a React round-trip to something the browser\n * already tracks natively, and can miss fast pointer movement.\n * `onOpenAutoFocus` is prevented so opening the menu (by clicking the trigger, or by typing `/`)\n * never steals focus away from the textarea — the user must be able to keep typing.\n */\n\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { clsx } from \"clsx\";\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport { MenuContent } from \"@pi-studio-ui/components/primitives/Menu.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport type { ComboboxOption } from \"@pi-studio-ui/ui/combobox.js\";\nimport styles from \"./ModelMenu.module.css\";\n\n/**\n * `onOpenAutoFocus` prevents Radix from auto-focusing the menu on open (needed so opening it —\n * by clicking the trigger, or by typing `/` — never steals focus away from the textarea, which\n * must be able to keep receiving keystrokes). It's real and runtime-forwarded — `MenuContentImpl`\n * destructures and composes it (`@radix-ui/react-menu`'s `dist/index.mjs`) — but Radix's PUBLIC\n * `DropdownMenuContentProps` type deliberately omits it (it's typed as\n * `MenuContentImplPrivateProps`, i.e. intentionally internal-API-only), so passing it as a plain\n * JSX attribute fails the excess-property check. Spreading a separately-typed object (rather than\n * inlining the prop) sidesteps that check honestly, without widening `DropdownMenu.Content`'s\n * props type as a whole.\n */\nconst preventOpenAutoFocus: { onOpenAutoFocus: (event: Event) => void } = {\n onOpenAutoFocus: (event) => event.preventDefault(),\n};\n\n/** Distinct color per command source so the three kinds read apart at a glance, not just by\n * their (tiny) label text — unmapped/future kinds fall back to `.commandKindBadge`'s own\n * neutral border/text color. */\nfunction kindBadgeClass(kind: string): string | undefined {\n switch (kind) {\n case \"extension\":\n return styles.commandKindExtension;\n case \"prompt\":\n return styles.commandKindPrompt;\n case \"skill\":\n return styles.commandKindSkill;\n default:\n return undefined;\n }\n}\n\nexport interface CommandMenuProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Rows to render, already filtered + ordered by the caller (`slash-commands.ts`). */\n options: ComboboxOption<string>[];\n /** Index into `options`; the row rendered as preselected. -1 for none. */\n highlightedIndex: number;\n onSelect: (name: string) => void;\n isLoading: boolean;\n isError: boolean;\n errorMessage?: string;\n /** Count of extension commands hidden because a turn is running; 0 renders no note. */\n hiddenExtensionCount: number;\n renderTrigger: () => ReactNode;\n}\n\nexport function CommandMenu({\n open,\n onOpenChange,\n options,\n highlightedIndex,\n onSelect,\n isLoading,\n isError,\n errorMessage,\n hiddenExtensionCount,\n renderTrigger,\n}: CommandMenuProps) {\n const listRef = useRef<HTMLDivElement>(null);\n\n // Keyboard nav (composer's ArrowUp/ArrowDown) moves `highlightedIndex` without touching scroll\n // — the list's own `overflow-y: auto` never follows it on its own, so a highlighted row outside\n // the visible 320px window stayed invisible until scrolled manually. `\"nearest\"` only scrolls\n // when the row is actually out of view, so it doesn't jitter while the row is already visible.\n useEffect(() => {\n if (!open) return;\n listRef.current?.querySelector('[aria-selected=\"true\"]')?.scrollIntoView({ block: \"nearest\" });\n }, [open, highlightedIndex]);\n\n return (\n <DropdownMenu.Root open={open} onOpenChange={onOpenChange} modal={false}>\n <DropdownMenu.Trigger asChild>{renderTrigger()}</DropdownMenu.Trigger>\n <MenuContent\n className={clsx(styles.picker, styles.commandContent)}\n side=\"top\"\n sideOffset={4}\n {...preventOpenAutoFocus}\n // Radix returns focus to the trigger button when the menu closes by default — that\n // would undo `applySelectedCommand`'s explicit `el.focus()` back onto the textarea\n // (Escape-to-close and click-away-to-close both go through this same path too), so the\n // textarea must own focus after ANY close, not just the apply path.\n onCloseAutoFocus={(e) => e.preventDefault()}\n >\n {isLoading && (\n <div className={styles.state}>\n <Spinner size=\"sm\" />\n </div>\n )}\n {!isLoading && isError && (\n <div className={styles.stateError}>{errorMessage ?? \"Failed to load commands\"}</div>\n )}\n {!isLoading && !isError && options.length === 0 && (\n <div className={styles.state}>No commands found</div>\n )}\n {!isLoading && !isError && options.length > 0 && (\n <div ref={listRef} className={clsx(styles.list, styles.commandList)}>\n {options.map((opt, i) => (\n <div\n key={opt.value}\n role=\"option\"\n aria-selected={i === highlightedIndex}\n className={clsx(\n styles.item,\n styles.commandItem,\n i === highlightedIndex && styles.itemActive,\n )}\n // Native title tooltip (the app's established hover-text convention, e.g.\n // Composer.tsx's icon buttons) shows the FULL description on hover — the row\n // itself only ever renders a 2-line clamp.\n title={opt.description}\n onMouseDown={(e) => {\n // Keep the textarea's focus/caret through the click — this menu never takes\n // keyboard focus, so a default mousedown would blur the textarea for nothing.\n e.preventDefault();\n onSelect(opt.value);\n }}\n >\n <div className={styles.commandItemHeader}>\n <span className={styles.label}>{opt.label}</span>\n {opt.kind && (\n <span className={clsx(styles.commandKindBadge, kindBadgeClass(opt.kind))}>\n {opt.kind}\n </span>\n )}\n </div>\n {opt.description && (\n <div className={styles.commandDescription}>{opt.description}</div>\n )}\n </div>\n ))}\n </div>\n )}\n {!isLoading && !isError && hiddenExtensionCount > 0 && (\n <div className={styles.state}>\n {hiddenExtensionCount} extension command(s) unavailable while the agent is running\n </div>\n )}\n </MenuContent>\n </DropdownMenu.Root>\n );\n}\n","/**\n * Pure slash-command logic for the composer's `/` picker (sprint-040 web-client half) — kept out\n * of `Composer.tsx`/`CommandMenu.tsx` so it's unit-testable under the root vitest config (which\n * only discovers `.test.ts` files under a node environment; `.tsx` component files are not\n * picked up there, following `model-menu-sort.ts`'s precedent).\n *\n * Every function here works with Pi's own token grammar\n * (`^\\/([^\\s]+)(?:\\s+([\\s\\S]*))?$`, `node_modules/@earendil-works/pi-coding-agent/dist/core/\n * prompt-templates.js:221-229`) and its exact, case-sensitive command-name matching\n * (`dist/core/extensions/runner.js:436-438`), so `knownCommandSpan`'s highlight doubles as a live\n * \"Pi will recognize this\" indicator rather than decoration.\n */\n\nimport type { ComboboxOption } from \"@pi-studio-ui/ui/combobox.js\";\n\n/** The fields of an `agent_list_commands_response` entry this module needs. */\nexport interface SlashCommand {\n name: string;\n description?: string;\n source?: string;\n scope?: string;\n}\n\n/** Leading `/token` of a composer draft; `end` is the index just past the token. */\nexport interface SlashToken {\n name: string;\n end: number;\n hasArgs: boolean;\n}\n\n/** `null` unless the draft's first character is `/`. `name` excludes the slash. */\nexport function parseSlashToken(text: string): SlashToken | null {\n if (!text.startsWith(\"/\")) return null;\n const match = /^\\/(\\S*)/.exec(text);\n const name = match?.[1] ?? \"\";\n const end = 1 + name.length;\n const hasArgs = /\\S/.test(text.slice(end));\n return { name, end, hasArgs };\n}\n\n/** True while the draft is still just a command name being typed: `/^\\/[^\\s]*$/`. */\nexport function shouldOpenMenu(text: string): boolean {\n return /^\\/\\S*$/.test(text);\n}\n\n/**\n * Menu rows in daemon order (no client-side sort — the daemon's order is Pi's order).\n * `running` drops `source === \"extension\"` entries: Pi rejects extension commands on `steer`/\n * `follow_up` (`agent-session.js` `_throwIfExtensionCommand`), which is the only send path while a\n * turn is in flight, and that rejection is a swallowed `notify` response, i.e. silent.\n */\nexport function commandOptions(\n commands: SlashCommand[],\n opts: { running: boolean },\n): { options: ComboboxOption<string>[]; hiddenExtensionCount: number } {\n let hiddenExtensionCount = 0;\n const options: ComboboxOption<string>[] = [];\n for (const cmd of commands) {\n if (opts.running && cmd.source === \"extension\") {\n hiddenExtensionCount += 1;\n continue;\n }\n options.push({\n value: cmd.name,\n label: `/${cmd.name}`,\n description: cmd.description,\n kind: cmd.source,\n });\n }\n return { options, hiddenExtensionCount };\n}\n\n/** Wrapping highlight movement; returns 0 for an empty list. */\nexport function moveHighlight(current: number, delta: number, length: number): number {\n if (length <= 0) return 0;\n return (current + delta + length) % length;\n}\n\n/**\n * Replace the draft's leading token with `/name`, keeping any args. With no args, append exactly\n * one space (which also makes `shouldOpenMenu` false, closing the menu by itself). With no leading\n * slash at all, prefix `/name ` to the existing text.\n */\nexport function applyCommand(text: string, name: string): string {\n const token = parseSlashToken(text);\n if (!token) return `/${name} ${text}`;\n if (!token.hasArgs) return `/${name} `;\n return `/${name}${text.slice(token.end)}`;\n}\n\n/**\n * The span to highlight in the textarea: the leading token only when it matches a known command\n * name exactly and case-sensitively — the same test Pi applies, so the highlight doubles as \"Pi\n * will recognize this\".\n */\nexport function knownCommandSpan(text: string, names: readonly string[]): { end: number } | null {\n const token = parseSlashToken(text);\n if (!token || !names.includes(token.name)) return null;\n return { end: token.end };\n}\n","/**\n * Composer — autosizing textarea + attach/send/stop (POC `initChatPanel`/`send()`,\n * POC_TO_APP_PLAN_UI.md §4.4). Every send goes through `ensureMaterialized` (`stores/\n * materialize.ts`) first: a brand-new session (`agentId: null`) becomes a real, persisted draft\n * via a no-`initialPrompt` `createAgent` call — the daemon persists the record but does NOT spawn\n * a provider process yet (`agent-service.ts` `handleCreate`'s deferred-draft branch); the process\n * spawns lazily on this very send (`spawnOrResumeSession`). `ensureMaterialized` is a no-op once\n * already bound, so this is the single path for both \"first message ever\" and every follow-up:\n * `client.agent(agentId).send(...)`. Stop → `client.agent(agentId).interrupt()`.\n *\n * Because the agent is bound (materialized) *before* this call, `useAgentStream` (attaches once\n * `agentId` is set) is already subscribed by the time `send`'s turn starts streaming — unlike the\n * old \"one RPC does create-agent-and-run-the-first-turn\" path, there is no window where the first\n * turn's events arrive on a raw broadcast nobody is listening to, so this file no longer needs a\n * first-turn gate/latch dance for a still-unbound session.\n *\n * OPTIMISTIC ECHO: `send_agent_prompt` blocks server-side until the whole turn finishes, and even\n * the *first* broadcast event only fires once the agent process is booted/warm — for a session\n * materializing on this very send, that means a real process-spawn+handshake before anything\n * appears (see AGENTS.md \"First-message latency\"). To hide that, `submit` mints a\n * `clientMessageId` and inserts the user's row into the timeline *synchronously*, before the RPC\n * is even issued. The daemon echoes the same id back verbatim as the `user_message` event's\n * `messageId` (`packages/server/src/agent/agent-service.ts` `runTurn`); the reducer\n * (`timeline/reducer.ts` `onUserMessage`) matches it against the pending row and confirms it in\n * place instead of appending a duplicate.\n *\n * MODEL & MATERIALIZATION: a chat materializes the instant its tab is created\n * (`tab-store.ts` `openNewChat` calls `ensureMaterialized`, which also resolves and pins the\n * default model into the record if none was set yet) — long before this composer even mounts, in\n * the common case. `submit`'s own `ensureMaterialized` call below is the retry path: it only does\n * real work when the eager materialize failed or never ran (opened while disconnected). Picking a\n * different model (`StatusBar.tsx`'s `handleSelectModel`) updates the already-materialized\n * record's `config.model`/`config.modelProvider` directly — replayed by the server on first spawn\n * regardless of whether it's the resolved default or an explicit pick (`spawnOrResumeSession`).\n *\n * STEERING: while the agent is running, the primary action becomes **Steer** instead of Send\n * (`send_agent_prompt` is only legal when idle — see AGENTS.md \"Steering\"). Steer reuses the exact\n * optimistic-echo + reconciliation path above (`clientMessageId` in, same `user_message` echo\n * back), just against `client.agent(id).steer(...)` instead of `.send(...)`, and marks the\n * optimistic row `queued: true` so `UserRow` can show a \"queued\" badge until a `queue_update`\n * stream event drops the text from its `steering[]` list (`timeline/reducer.ts`'s\n * `onQueueUpdate`). Follow-up (`.followUp(...)`, delivered only after the turn fully stops) is\n * intentionally not surfaced here — SDK/CLI only for now. Steering only makes sense against a\n * live agent — never triggers materialization itself.\n */\n\nimport { useRef, useState, type ChangeEvent, type ClipboardEvent, type KeyboardEvent } from \"react\";\nimport { clsx } from \"clsx\";\nimport { Navigation, Paperclip, Send, Slash, Square } from \"lucide-react\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { TextArea } from \"@pi-studio-ui/components/primitives/TextInput.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { ensureMaterialized } from \"@pi-studio-ui/stores/materialize.js\";\nimport { useAgentCommands } from \"@pi-studio-ui/hooks/use-agent-commands.js\";\nimport { filterOptions } from \"@pi-studio-ui/ui/combobox.js\";\nimport { Attachments, readImageFile, type PendingImage } from \"./Attachments.js\";\nimport { CommandMenu } from \"./CommandMenu.js\";\nimport {\n applyCommand,\n commandOptions,\n knownCommandSpan,\n moveHighlight,\n parseSlashToken,\n shouldOpenMenu,\n} from \"./slash-commands.js\";\nimport styles from \"./Composer.module.css\";\n\nconst MAX_TEXTAREA_HEIGHT = 160;\n\nexport interface ComposerProps {\n sessionId: string;\n}\n\nfunction autoResize(el: HTMLTextAreaElement): void {\n el.style.height = \"auto\";\n el.style.height = `${Math.min(el.scrollHeight, MAX_TEXTAREA_HEIGHT)}px`;\n}\n\n/** `steer_agent_response`'s `{ ok: false }` — no live turn, or the provider lacks steering. */\nfunction isOkFalse(value: unknown): boolean {\n return typeof value === \"object\" && value !== null && \"ok\" in value && value.ok === false;\n}\n\nexport function Composer({ sessionId }: ComposerProps) {\n const client = useConnectionStore((s) => s.client);\n const session = useSessionStore((s) => s.sessions[sessionId]);\n const addOptimisticUserMessage = useSessionStore((s) => s.addOptimisticUserMessage);\n const markUserMessageFailed = useSessionStore((s) => s.markUserMessageFailed);\n const setCwd = useSessionStore((s) => s.setCwd);\n\n const [text, setText] = useState(\"\");\n const [images, setImages] = useState<PendingImage[]>([]);\n // Two independent busy flags: `sending` guards Send/create-agent, whose RPC blocks\n // server-side for the *entire* turn (`runTurn` doesn't resolve until the turn ends — see\n // AGENTS.md \"Steering\"); `steering` guards the separate, fire-and-forget steer RPC. Sharing\n // one flag would leave the Steer button disabled for the whole turn, since the original\n // send's promise stays pending throughout it.\n const [sending, setSending] = useState(false);\n const [steering, setSteering] = useState(false);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [menuOpen, setMenuOpen] = useState(false);\n const [highlight, setHighlight] = useState(0);\n\n const running = session?.status === \"running\";\n const busy = running ? steering : sending;\n const canSubmit = Boolean(client) && !busy && (text.trim().length > 0 || images.length > 0);\n\n // Read-through cached exactly like `use-provider-models.ts` (see the hook's own docstring):\n // reopening the `/` menu — including the auto-open that fires on every `/` keystroke — shows\n // the cached rows immediately instead of a spinner every time.\n const { data: commands = [], isLoading, isError, error } = useAgentCommands(sessionId, menuOpen);\n const { options, hiddenExtensionCount } = commandOptions(commands, { running });\n const token = parseSlashToken(text);\n const filtered = token ? filterOptions(options, token.name) : options;\n const commandNames = commands.map((c) => c.name);\n // The span to highlight in the textarea — only a token Pi will actually recognize as a command.\n const span = knownCommandSpan(text, commandNames);\n\n async function addImageFile(file: File): Promise<void> {\n if (!file.type.startsWith(\"image/\")) return;\n try {\n const image = await readImageFile(file);\n setImages((prev) => [...prev, image]);\n } catch {\n // Best-effort attachment read — ignore unreadable files (POC has no error UI here either).\n }\n }\n\n function handleFileInputChange(ev: ChangeEvent<HTMLInputElement>): void {\n for (const file of ev.target.files ?? []) void addImageFile(file);\n ev.target.value = \"\";\n }\n\n function handlePaste(ev: ClipboardEvent<HTMLTextAreaElement>): void {\n for (const item of ev.clipboardData.items) {\n if (item.type.startsWith(\"image/\")) {\n ev.preventDefault();\n const file = item.getAsFile();\n if (file) void addImageFile(file);\n }\n }\n }\n\n function handleRemoveImage(index: number): void {\n setImages((prev) => prev.filter((_, i) => i !== index));\n }\n\n function handleTextareaChange(ev: ChangeEvent<HTMLTextAreaElement>): void {\n const next = ev.target.value;\n setText(next);\n autoResize(ev.target);\n // `/` at the very start opens the menu and keeps it open while the name is still being typed;\n // the first space (or any non-slash draft) closes it. Never reopens for a `/` mid-text — Pi\n // only recognizes a command at index 0 (`agent-session.js` `text.startsWith(\"/\")`).\n const shouldOpen = shouldOpenMenu(next);\n if (shouldOpen !== menuOpen) setMenuOpen(shouldOpen);\n if (shouldOpen) setHighlight(0);\n }\n\n function handleKeyDown(ev: KeyboardEvent<HTMLTextAreaElement>): void {\n // A single Backspace deletes the WHOLE recognized command token, like a chip/mention, instead\n // of forcing the user to peck through it one character at a time. Scoped to `!menuOpen` (the\n // command has already been accepted/typed in full and the menu closed) so it never fights\n // the ArrowDown/Enter accept flow below while the user is still actively narrowing a match.\n if (!menuOpen && span && ev.key === \"Backspace\") {\n const el = ev.currentTarget;\n const caret = el.selectionStart;\n if (caret !== null && caret === el.selectionEnd) {\n // Also swallow the single trailing space `applyCommand` appends, so the draft goes\n // straight back to empty (or to whatever args followed) in one keystroke, not two.\n const deletableEnd = span.end + (text[span.end] === \" \" ? 1 : 0);\n if (caret > 0 && caret <= deletableEnd) {\n ev.preventDefault();\n const next = text.slice(deletableEnd);\n setText(next);\n el.setSelectionRange(0, 0);\n autoResize(el);\n return;\n }\n }\n }\n if (menuOpen && filtered.length > 0) {\n if (ev.key === \"ArrowDown\" || ev.key === \"ArrowUp\") {\n ev.preventDefault();\n setHighlight((i) => moveHighlight(i, ev.key === \"ArrowDown\" ? 1 : -1, filtered.length));\n return;\n }\n // Enter and Tab both accept the preselected command — they complete the token, they do NOT\n // send. The user still has to press Enter again to submit, so a command can take arguments.\n if ((ev.key === \"Enter\" && !ev.shiftKey) || ev.key === \"Tab\") {\n ev.preventDefault();\n const name = filtered[highlight]?.value;\n if (name) applySelectedCommand(name);\n return;\n }\n }\n if (menuOpen && ev.key === \"Escape\") {\n ev.preventDefault();\n setMenuOpen(false);\n return;\n }\n if (ev.key === \"Enter\" && !ev.shiftKey) {\n ev.preventDefault();\n void submit(running ? \"steer\" : \"send\");\n }\n }\n\n function applySelectedCommand(name: string): void {\n const next = applyCommand(text, name);\n setText(next);\n setMenuOpen(false);\n setHighlight(0);\n const el = textareaRef.current;\n if (el) {\n el.focus();\n el.setSelectionRange(next.length, next.length);\n autoResize(el);\n }\n }\n\n async function submit(mode: \"send\" | \"steer\"): Promise<void> {\n if (!client || !session) return;\n if (mode === \"steer\" ? steering : sending) return;\n // Steering only makes sense against a live agent — a running session always has one.\n if (mode === \"steer\" && !session.agentId) return;\n const trimmed = text.trim();\n if (!trimmed && images.length === 0) return;\n\n setText(\"\");\n // A bare Enter (or the Send/Steer button) can fire while the menu is still open — e.g. the\n // draft is a command-like token with no filter match yet (`filtered.length === 0`), so the\n // accept branch in `handleKeyDown` never ran. Close it here too, not just in `applyCommand`'s\n // trailing-space path, so a send never leaves the menu stuck open over an empty draft.\n setMenuOpen(false);\n if (textareaRef.current) autoResize(textareaRef.current);\n const sentImages = images;\n setImages([]);\n\n const prompt = trimmed || \"Describe this image\";\n const rpcImages =\n sentImages.length > 0\n ? sentImages.map((img) => ({ mimeType: img.mimeType, data: img.data }))\n : undefined;\n\n // Optimistic echo: render the user's row synchronously instead of waiting for the daemon's\n // `user_message` broadcast (see file header). `clientMessageId` is round-tripped through the\n // RPC below and echoed back verbatim as the broadcast event's `messageId`, which is how the\n // reducer reconciles this row instead of appending a duplicate once the server confirms it.\n // Steered rows are additionally marked `queued: true` (cleared by a later `queue_update`).\n const clientMessageId = crypto.randomUUID();\n addOptimisticUserMessage(sessionId, clientMessageId, prompt, rpcImages, mode === \"steer\");\n\n const setBusy = mode === \"steer\" ? setSteering : setSending;\n setBusy(true);\n try {\n if (mode === \"steer\") {\n // Fire-and-forget injection into the live turn — never touches materialization, which\n // only applies to the send-only, no-agent-yet path below.\n const result = await client\n .agent(session.agentId!)\n .steer(prompt, { clientMessageId, images: rpcImages });\n if (isOkFalse(result)) markUserMessageFailed(sessionId, clientMessageId);\n } else {\n if (!session.agentId) setCwd(sessionId, session.cwd || \"~\");\n // Retry path (see file header \"MODEL & MATERIALIZATION\") for a session whose eager\n // materialize (`tab-store.ts` `openNewChat`) never ran or failed — a no-op once already\n // bound. `useAgentStream` attaches the instant `agentId` is bound, before this turn's\n // first event can possibly arrive (see file header), so `send` needs no separate\n // first-turn broadcast path the way a combined create-and-run RPC used to.\n const agentId = await ensureMaterialized(client, sessionId);\n await client.agent(agentId).send(prompt, { clientMessageId, images: rpcImages });\n }\n } catch {\n // RPC rejection before any `user_message` broadcast arrived (e.g. dropped connection) —\n // the optimistic row would otherwise stay pending forever with no feedback. If the server\n // *did* manage to broadcast `user_message` first (the row is already reconciled), this is\n // a no-op — `markUserMessageFailed` only touches a still-pending row. Turn-level failures\n // that occur after that point still surface via `agent_stream` `error`/`turn_failed`\n // events (POC swallows the RPC rejection the same way; the stream is the source of truth\n // for those).\n markUserMessageFailed(sessionId, clientMessageId);\n } finally {\n setBusy(false);\n }\n }\n\n function handleStop(): void {\n if (!session?.agentId) return;\n void client?.agent(session.agentId).interrupt();\n }\n\n return (\n <div className={styles.composer}>\n <CommandMenu\n open={menuOpen}\n onOpenChange={setMenuOpen}\n options={filtered}\n highlightedIndex={highlight}\n onSelect={applySelectedCommand}\n isLoading={isLoading}\n isError={isError}\n errorMessage={error instanceof Error ? error.message : undefined}\n hiddenExtensionCount={hiddenExtensionCount}\n renderTrigger={() => (\n <Button\n className={styles.slashBtn}\n variant=\"ghost\"\n size=\"md\"\n iconOnly\n title=\"Slash commands\"\n aria-label=\"Slash commands\"\n >\n <Slash size={16} />\n </Button>\n )}\n />\n <div className={styles.inputArea}>\n <div className={styles.textareaWrap}>\n <div className={styles.highlightLayer} aria-hidden>\n {span ? (\n <>\n <mark className={styles.commandMark}>{text.slice(0, span.end)}</mark>\n {text.slice(span.end)}\n </>\n ) : (\n text\n )}\n </div>\n <TextArea\n ref={textareaRef}\n className={clsx(styles.textarea, span && styles.textareaOverlaid)}\n rows={1}\n value={text}\n placeholder={\n running\n ? \"Steer the running turn… (Enter to steer, Shift+Enter for newline)\"\n : \"Ask anything… (Enter to send, Shift+Enter for newline)\"\n }\n onChange={handleTextareaChange}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n />\n </div>\n <Attachments images={images} onRemove={handleRemoveImage} />\n </div>\n <Button\n className={styles.attachBtn}\n variant=\"ghost\"\n size=\"md\"\n iconOnly\n title=\"Attach image\"\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip size={16} />\n </Button>\n <input\n ref={fileInputRef}\n className={styles.hiddenFileInput}\n type=\"file\"\n accept=\"image/*\"\n multiple\n onChange={handleFileInputChange}\n />\n <div className={styles.actions}>\n {running ? (\n <Button\n size=\"sm\"\n disabled={!canSubmit}\n onClick={() => void submit(\"steer\")}\n leftIcon={<Navigation size={14} />}\n >\n Steer\n </Button>\n ) : (\n <Button\n size=\"sm\"\n disabled={!canSubmit}\n onClick={() => void submit(\"send\")}\n leftIcon={<Send size={14} />}\n >\n Send\n </Button>\n )}\n {running && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n onClick={handleStop}\n leftIcon={<Square size={14} />}\n >\n Stop\n </Button>\n )}\n </div>\n </div>\n );\n}\n","/**\n * ChatPanel — the tab content mounted by `panel-registry.ts` for `kind: \"chat\"` tabs. Looks up\n * the session by `tab.data.sessionId`, keeps its timeline live via `useAgentStream`, and renders\n * the virtualized `Timeline` + `Composer` (POC_TO_APP_PLAN_UI.md §4.3/§4.4).\n */\n\nimport { useAgentStream } from \"@pi-studio-ui/hooks/use-agent-stream.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport type { Tab, ChatTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { Timeline } from \"./Timeline.js\";\nimport { Composer } from \"./Composer.js\";\nimport styles from \"./ChatPanel.module.css\";\n\nexport interface ChatPanelProps {\n tab: Tab;\n owningPaneId: string | null;\n}\n\nexport function ChatPanel({ tab, owningPaneId }: ChatPanelProps) {\n // `panel-registry.ts` only ever mounts this component for `tab.kind === \"chat\"`, whose `data`\n // is always `ChatTabData` — narrowed here since `Tab[\"data\"]` is a union at the type level.\n const { sessionId } = tab.data as ChatTabData;\n const session = useSessionStore((s) => s.sessions[sessionId]);\n\n useAgentStream(sessionId);\n\n if (!session) {\n return (\n <Panel>\n <div className={styles.empty}>Session not found.</div>\n </Panel>\n );\n }\n\n return (\n <Panel>\n <Timeline session={session} owningPaneId={owningPaneId} workspaceCwd={tab.workspaceCwd} />\n <Composer sessionId={session.id} />\n </Panel>\n );\n}\n"],"names":["ICON_BY_KIND","Terminal","Eye","Pencil","FilePlus","Search","Globe","ListTree","toolIcon","tool","toolLabel","toolFilePath","toolMutatesFiles","toolBody","sections","text","toolDetailText","toolInlineDetail","parts","DEBOUNCE_MS","pendingTimer","invalidateAfterToolCompletion","queryClient","client","cwd","_toolFilePath","applyAgentStreamEvent","sessionId","event","sessionStore","useSessionStore","session","useAgentStream","useConnectionStore","s","agentId","_a","useQueryClient","useEffect","AssistantRow","row","assetBase","owningPaneId","workspaceCwd","jsxs","styles","jsx","Markdown","ReasoningRow","BADGE_VARIANT","ToolCard","expanded","setExpanded","useState","Icon","inlineDetail","body","hasBody","clsx","v","ChevronRight","StatusBadge","section","i","DiffView","UserRow","openedSrc","setOpenedSrc","stateClass","Clock","img","src","ev","Dialog","open","ErrorRow","SystemRow","STICK_THRESHOLD_PX","renderRow","Timeline","rows","running","homeDir","useHomeDir","normalizeCwd","scrollRef","useRef","stickToBottomRef","prevRowCountRef","virtualizer","useVirtualizer","index","element","entry","instance","measureElementDefault","key","handleScroll","el","distanceFromBottom","prevCount","grew","virtualRow","useAgentCommands","enabled","useQuery","rpcKeys","ensureMaterialized","commands","Attachments","images","onRemove","readImageFile","file","promise","resolve","reject","reader","result","commaIndex","data","preventOpenAutoFocus","kindBadgeClass","kind","CommandMenu","onOpenChange","options","highlightedIndex","onSelect","isLoading","isError","errorMessage","hiddenExtensionCount","renderTrigger","listRef","_b","DropdownMenu.Root","DropdownMenu.Trigger","MenuContent","e","Spinner","opt","parseSlashToken","match","name","end","hasArgs","shouldOpenMenu","commandOptions","opts","cmd","moveHighlight","current","delta","length","applyCommand","token","knownCommandSpan","names","MAX_TEXTAREA_HEIGHT","autoResize","isOkFalse","value","Composer","addOptimisticUserMessage","markUserMessageFailed","setCwd","setText","setImages","sending","setSending","steering","setSteering","textareaRef","fileInputRef","menuOpen","setMenuOpen","highlight","setHighlight","canSubmit","error","filtered","filterOptions","commandNames","c","span","addImageFile","image","prev","handleFileInputChange","handlePaste","item","handleRemoveImage","_","handleTextareaChange","next","shouldOpen","handleKeyDown","caret","deletableEnd","submit","applySelectedCommand","mode","trimmed","sentImages","prompt","rpcImages","clientMessageId","setBusy","handleStop","Button","Slash","Fragment","TextArea","Paperclip","Navigation","Send","Square","ChatPanel","tab","Panel"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var f=Object.defineProperty;var d=(e,r,t)=>r in e?f(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var n=(e,r,t)=>d(e,typeof r!="symbol"?r+"":r,t);import{u as m}from"./vendor-query-BGIh-vlp.js";import{a as p,r as g}from"./index-
|
|
2
|
-
//# sourceMappingURL=CodeView-
|
|
1
|
+
var f=Object.defineProperty;var d=(e,r,t)=>r in e?f(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var n=(e,r,t)=>d(e,typeof r!="symbol"?r+"":r,t);import{u as m}from"./vendor-query-BGIh-vlp.js";import{a as p,r as g}from"./index-KQwi4ZXj.js";import{j as u,r as l}from"./vendor-react-CWo7Do4s.js";import{O as h,P as w,Q as x,S as y,T as E,U as _}from"./vendor-04vXFIXe.js";class F extends Error{constructor(t,a){super("file_too_large");n(this,"size");n(this,"maxBytes");this.name="FileTooLargeError",this.size=t,this.maxBytes=a}}function R(e){if(!e.ok)throw e.error==="file_too_large"?new F(e.size??0,e.maxBytes):new Error(e.error??"file read failed");return{content:e.content??"",size:e.size??0}}function v(e,r=!0){const t=p(a=>a.client);return m({queryKey:g.fileRead(e),queryFn:async()=>{if(!t)throw new Error("not connected");const a=await t.connection.request("file_read_request",{path:e});return R(a)},enabled:!!t&&!!e&&r})}const z="_wrap_1feu3_1",B={wrap:z},c=new Map;function C(e){const[r,t]=l.useState(null);return l.useEffect(()=>{const a=E.matchFilename(_,e);if(!a){t(null);return}const s=c.get(a.name);if(s){t(s);return}let o=!1;return t(null),a.load().then(i=>{c.set(a.name,i),o||t(i)}),()=>{o=!0}},[e]),r?[r]:[]}function K({path:e,content:r}){const t=C(e),a=[h.lineWrapping,w(),...t];return u.jsx("div",{className:B.wrap,children:u.jsx(x,{value:r,theme:y,extensions:a,editable:!1,readOnly:!0,basicSetup:{highlightActiveLine:!1,foldGutter:!1,searchKeymap:!1},height:"100%"})})}export{K as C,F,v as u};
|
|
2
|
+
//# sourceMappingURL=CodeView-CXE9vlF1.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeView-
|
|
1
|
+
{"version":3,"file":"CodeView-CXE9vlF1.js","sources":["../../../src/hooks/use-file-read.ts","../../../src/features/files/CodeView.tsx"],"sourcesContent":["/**\n * `use-file-read` — TanStack Query wrapper over `file_read_request` (POC `loadFileContent`,\n * POC_TO_APP_PLAN_UI.md §4.5 / §3 `hooks/use-file-read.ts`).\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface FileReadResult {\n content: string;\n size: number;\n}\n\n/** Thrown by `useFileRead` when the server rejects a read with `error: \"file_too_large\"`\n * (`file_read_response`, `MAX_INLINE_FILE_READ_BYTES` server-side). Carries `size` and the\n * server's own `maxBytes` ceiling so a caller can render an accurate message and decide whether\n * to fall back to the streaming path (`use-file-text.ts`) instead of just displaying the string\n * `\"file_too_large\"`. */\nexport class FileTooLargeError extends Error {\n readonly size: number;\n readonly maxBytes?: number;\n\n constructor(size: number, maxBytes?: number) {\n super(\"file_too_large\");\n this.name = \"FileTooLargeError\";\n this.size = size;\n this.maxBytes = maxBytes;\n }\n}\n\nexport interface FileReadRpcResponse {\n ok: boolean;\n content?: string;\n size?: number;\n maxBytes?: number;\n error?: string;\n}\n\n/** Maps a raw `file_read_response` into a result or a thrown error — pure, so the\n * `file_too_large` classification (the actual behavior change in this task) is directly\n * unit-testable without a connected client or React. */\nexport function parseFileReadResponse(response: FileReadRpcResponse): FileReadResult {\n if (!response.ok) {\n if (response.error === \"file_too_large\") {\n throw new FileTooLargeError(response.size ?? 0, response.maxBytes);\n }\n throw new Error(response.error ?? \"file read failed\");\n }\n return { content: response.content ?? \"\", size: response.size ?? 0 };\n}\n\nexport function useFileRead(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.fileRead(path),\n queryFn: async (): Promise<FileReadResult> => {\n if (!client) throw new Error(\"not connected\");\n const response = await client.connection.request<FileReadRpcResponse>(\"file_read_request\", {\n path,\n });\n return parseFileReadResponse(response);\n },\n enabled: Boolean(client) && Boolean(path) && enabled,\n });\n}\n","/**\n * CodeView — highlighted, read-only file body with a line-number gutter (POC `showFile`/\n * `reloadFileInPanel`, chat.html ~line 812-828, POC_TO_APP_PLAN_UI.md §4.5). Built on CodeMirror 6\n * (`@uiw/react-codemirror`) rather than a hand-rolled `<pre>` + separate gutter `<pre>`: the two\n * previously drifted out of sync on scroll (independent scroll containers, no shared scroll\n * anchor) whenever content was long enough to scroll or lines wrapped. CodeMirror owns a single\n * scrollable viewport with gutter and content painted from the same layout pass, so this class of\n * bug cannot recur.\n */\n\nimport { useEffect, useState } from \"react\";\nimport CodeMirror from \"@uiw/react-codemirror\";\nimport type { Extension } from \"@codemirror/state\";\nimport { EditorView } from \"@codemirror/view\";\nimport { languages } from \"@codemirror/language-data\";\nimport { LanguageDescription, type LanguageSupport, foldGutter } from \"@codemirror/language\";\nimport { githubDark } from \"@uiw/codemirror-theme-github\";\nimport styles from \"./CodeView.module.css\";\n\nexport interface CodeViewProps {\n path: string;\n content: string;\n}\n\n// `LanguageDescription.support` is populated lazily by `.load()`, which dynamically imports the\n// grammar the first time a given language is used and resolves instantly (cached) after that.\nconst supportCache = new Map<string, LanguageSupport>();\n\nfunction useLanguageExtension(path: string): Extension[] {\n const [support, setSupport] = useState<LanguageSupport | null>(null);\n\n useEffect(() => {\n const desc = LanguageDescription.matchFilename(languages, path);\n if (!desc) {\n setSupport(null);\n return;\n }\n const cached = supportCache.get(desc.name);\n if (cached) {\n setSupport(cached);\n return;\n }\n let cancelled = false;\n setSupport(null);\n void desc.load().then((loaded) => {\n supportCache.set(desc.name, loaded);\n if (!cancelled) setSupport(loaded);\n });\n return () => {\n cancelled = true;\n };\n }, [path]);\n\n return support ? [support] : [];\n}\n\nexport function CodeView({ path, content }: CodeViewProps) {\n const languageExt = useLanguageExtension(path);\n const extensions: Extension[] = [EditorView.lineWrapping, foldGutter(), ...languageExt];\n\n return (\n <div className={styles.wrap}>\n <CodeMirror\n value={content}\n theme={githubDark}\n extensions={extensions}\n editable={false}\n readOnly\n basicSetup={{\n highlightActiveLine: false,\n foldGutter: false,\n searchKeymap: false,\n }}\n height=\"100%\"\n />\n </div>\n );\n}\n"],"names":["FileTooLargeError","size","maxBytes","__publicField","parseFileReadResponse","response","useFileRead","path","enabled","client","useConnectionStore","s","useQuery","rpcKeys","supportCache","useLanguageExtension","support","setSupport","useState","useEffect","desc","LanguageDescription","languages","cached","cancelled","loaded","CodeView","content","languageExt","extensions","EditorView","foldGutter","jsx","styles","CodeMirror","githubDark"],"mappings":"oYAmBO,MAAMA,UAA0B,KAAM,CAI3C,YAAYC,EAAcC,EAAmB,CAC3C,MAAM,gBAAgB,EAJfC,EAAA,aACAA,EAAA,iBAIP,KAAK,KAAO,oBACZ,KAAK,KAAOF,EACZ,KAAK,SAAWC,CAClB,CACF,CAaO,SAASE,EAAsBC,EAA+C,CACnF,GAAI,CAACA,EAAS,GACZ,MAAIA,EAAS,QAAU,iBACf,IAAIL,EAAkBK,EAAS,MAAQ,EAAGA,EAAS,QAAQ,EAE7D,IAAI,MAAMA,EAAS,OAAS,kBAAkB,EAEtD,MAAO,CAAE,QAASA,EAAS,SAAW,GAAI,KAAMA,EAAS,MAAQ,CAAA,CACnE,CAEO,SAASC,EAAYC,EAAcC,EAAU,GAAM,CACxD,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAEjD,OAAOC,EAAS,CACd,SAAUC,EAAQ,SAASN,CAAI,EAC/B,QAAS,SAAqC,CAC5C,GAAI,CAACE,EAAQ,MAAM,IAAI,MAAM,eAAe,EAC5C,MAAMJ,EAAW,MAAMI,EAAO,WAAW,QAA6B,oBAAqB,CACzF,KAAAF,CAAA,CACD,EACD,OAAOH,EAAsBC,CAAQ,CACvC,EACA,QAAS,EAAQI,GAAW,EAAQF,GAASC,CAAA,CAC9C,CACH,oCCxCMM,MAAmB,IAEzB,SAASC,EAAqBR,EAA2B,CACvD,KAAM,CAACS,EAASC,CAAU,EAAIC,EAAAA,SAAiC,IAAI,EAEnEC,OAAAA,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAOC,EAAoB,cAAcC,EAAWf,CAAI,EAC9D,GAAI,CAACa,EAAM,CACTH,EAAW,IAAI,EACf,MACF,CACA,MAAMM,EAAST,EAAa,IAAIM,EAAK,IAAI,EACzC,GAAIG,EAAQ,CACVN,EAAWM,CAAM,EACjB,MACF,CACA,IAAIC,EAAY,GAChB,OAAAP,EAAW,IAAI,EACVG,EAAK,KAAA,EAAO,KAAMK,GAAW,CAChCX,EAAa,IAAIM,EAAK,KAAMK,CAAM,EAC7BD,GAAWP,EAAWQ,CAAM,CACnC,CAAC,EACM,IAAM,CACXD,EAAY,EACd,CACF,EAAG,CAACjB,CAAI,CAAC,EAEFS,EAAU,CAACA,CAAO,EAAI,CAAA,CAC/B,CAEO,SAASU,EAAS,CAAE,KAAAnB,EAAM,QAAAoB,GAA0B,CACzD,MAAMC,EAAcb,EAAqBR,CAAI,EACvCsB,EAA0B,CAACC,EAAW,aAAcC,EAAA,EAAc,GAAGH,CAAW,EAEtF,OACEI,EAAAA,IAAC,MAAA,CAAI,UAAWC,EAAO,KACrB,SAAAD,EAAAA,IAACE,EAAA,CACC,MAAOP,EACP,MAAOQ,EACP,WAAAN,EACA,SAAU,GACV,SAAQ,GACR,WAAY,CACV,oBAAqB,GACrB,WAAY,GACZ,aAAc,EAAA,EAEhB,OAAO,MAAA,CAAA,EAEX,CAEJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as f,j as s}from"./vendor-react-CWo7Do4s.js";import{C as v}from"./vendor-04vXFIXe.js";import{a as j,r as d,c as w,d as E,P as D,B as x,V as K,E as l,S as g}from"./index-
|
|
2
|
-
//# sourceMappingURL=FilePanel-
|
|
1
|
+
import{r as f,j as s}from"./vendor-react-CWo7Do4s.js";import{C as v}from"./vendor-04vXFIXe.js";import{a as j,r as d,c as w,d as E,P as D,B as x,V as K,E as l,S as g}from"./index-KQwi4ZXj.js";import{u as S,c as k}from"./vendor-query-BGIh-vlp.js";import{u as q}from"./use-file-watch-DTOtoP8W.js";import{D as z}from"./DiffView-CrQA2QN6.js";import"./vendor-markdown-YvkQLm_s.js";import"./vendor-highlight-Efc6A-g6.js";import"./vendor-icons-CJ_dOXcR.js";import"./vendor-overlays-BnmaUbKw.js";import"./vendor-dnd-Bj2_GhfD.js";function N(e,a,r,i=!0){const t=j(n=>n.client);return S({queryKey:d.fileDiff(e,a,r),queryFn:async()=>{if(!t)throw new Error("not connected");return{patch:(await t.connection.request("file_diff_request",{path:e,cwd:a,staged:r})).patch??""}},enabled:!!t&&!!e&&!!a&&i})}const b=new Set(["text","markdown","image"]);function B(e,a,r){const i=k(),t=b.has(r)?w(e,a):null,{changedAt:n}=q(t);f.useEffect(()=>{n!==null&&(i.invalidateQueries({queryKey:d.fileRead(e)}),i.invalidateQueries({queryKey:d.fileDownload(e)}),i.invalidateQueries({queryKey:d.fileDiffByPath(e)}))},[n,e,i])}const C="_header_10rse_1",F="_path_10rse_11",V="_size_10rse_18",P="_viewToggle_10rse_23",Q="_body_10rse_28",c={header:C,path:F,size:V,viewToggle:P,body:Q};function y(e){return"path"in e}function R(e){return"staged"in e}function U({tab:e}){var m;const a=e.kind==="diff",r=e.data,i=y(r)?r.path:"",t=y(r)&&R(r)?r.staged:!1,[n,u]=f.useState(a?"diff":"file"),h=e.workspaceCwd||"~",p=E(i);B(i,h,p);const o=N(i,h,t,n==="diff"),_=K[p];return s.jsxs(D,{children:[s.jsxs("div",{className:c.header,children:[s.jsx("span",{className:c.path,children:i}),s.jsx("span",{className:c.size,children:n==="diff"?t?"staged":"unstaged":null}),s.jsxs("div",{className:c.viewToggle,children:[s.jsx(x,{size:"xs",variant:n==="file"?"default":"ghost",onClick:()=>u("file"),children:"File"}),s.jsx(x,{size:"xs",variant:n==="diff"?"default":"ghost",onClick:()=>u("diff"),children:"Diff"})]})]}),s.jsx("div",{className:v(c.body,"file-body"),children:n==="file"?s.jsx(f.Suspense,{fallback:s.jsxs(l,{children:[s.jsx(g,{size:"sm"})," Loading..."]}),children:s.jsx(_,{path:i})}):o.isLoading?s.jsxs(l,{children:[s.jsx(g,{size:"sm"})," Loading diff..."]}):o.isError?s.jsxs(l,{children:["Error: ",o.error instanceof Error?o.error.message:"unknown error"]}):(m=o.data)!=null&&m.patch?s.jsx(z,{patch:o.data.patch}):s.jsx(l,{style:{padding:"var(--pi-spacing-30)"},children:"No changes (file matches HEAD)"})})]})}export{U as FilePanel};
|
|
2
|
+
//# sourceMappingURL=FilePanel-jN6FYiGo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FilePanel-Ck53TDKW.js","sources":["../../../src/hooks/use-file-diff.ts","../../../src/hooks/use-file-live-refresh.ts","../../../src/features/files/FilePanel.tsx"],"sourcesContent":["/**\n * `use-file-diff` — TanStack Query wrapper over `file_diff_request` (POC `loadDiffContent`,\n * POC_TO_APP_PLAN_UI.md §4.5 / §3 `hooks/use-file-diff.ts`).\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface FileDiffResult {\n patch: string;\n}\n\ninterface FileDiffRpcResponse {\n patch?: string;\n}\n\nexport function useFileDiff(path: string, cwd: string, staged: boolean, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.fileDiff(path, cwd, staged),\n queryFn: async (): Promise<FileDiffResult> => {\n if (!client) throw new Error(\"not connected\");\n const response = await client.connection.request<FileDiffRpcResponse>(\"file_diff_request\", {\n path,\n cwd,\n staged,\n });\n return { patch: response.patch ?? \"\" };\n },\n enabled: Boolean(client) && Boolean(path) && Boolean(cwd) && enabled,\n });\n}\n","/**\n * `use-file-live-refresh` — watches a file tab's OWN file (via `useFileWatch`/`watchFile`) and\n * invalidates that path's content queries on every `file_changed` push, so a `kind:\"file\"` or\n * `kind:\"diff\"` tab refetches from disk within ~1s of an external change — an editor, a shell\n * command, a build step, or `git` run in a terminal — not just a Pi tool call completing\n * (`lib/connection/files-changed.ts`'s existing debounced post-tool-call invalidation, which stays\n * as the belt-and-braces path for files nobody has open).\n *\n * `LIVE_REFRESH_KINDS` is the scope gate: only `\"text\"`, `\"markdown\"`, and `\"image\"` tabs watch.\n * `\"video\"` is excluded because a refetch mints a new object URL, which restarts playback from\n * zero mid-watch; `\"binary\"` fetches nothing at all until the user clicks its Download button, so\n * there is nothing to refresh. Exported so the gate itself is unit-testable — a new `ViewerKind`\n * must make an explicit choice here, not inherit one.\n */\n\nimport { useEffect } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { resolveWorkspacePath } from \"@pi-studio-ui/lib/paths.js\";\nimport type { ViewerKind } from \"@pi-studio-ui/features/files/viewer-registry.js\";\nimport { useFileWatch } from \"./use-file-watch.js\";\n\nexport const LIVE_REFRESH_KINDS: ReadonlySet<ViewerKind> = new Set([\"text\", \"markdown\", \"image\"]);\n\n/**\n * Invalidates `path`'s `fileRead`/`fileDownload`/`fileDiffByPath` queries on every `file_changed`\n * push for the WATCHED (resolved absolute) target — these differ for a diff tab, which is why\n * both `path` and `cwd` are taken separately. All three keys are invalidated unconditionally: a\n * watched tab can be showing any of them (`fileRead` → `TextViewer` tier 1 + `MarkdownFileViewer`;\n * `fileDownload` → `ImageViewer` and `TextViewer`'s tier-2 streamed source; `fileDiffByPath` →\n * `DiffView`, reachable from both tab kinds through `FilePanel`'s File/Diff toggle). Invalidating a\n * key with no live query is a no-op, so only *whether to watch* branches on `viewerKind`, never\n * the invalidation list.\n *\n * No client-side debounce: the daemon already coalesces at `FILE_WATCH_COALESCE_MS` (150ms). A\n * deleted file pushes too — the refetch then fails and the viewer shows its existing error state,\n * which is the correct observable outcome; no special-casing.\n */\nexport function useFileLiveRefresh(path: string, cwd: string, viewerKind: ViewerKind): void {\n const queryClient = useQueryClient();\n // A `null` target subscribes nothing (`useFileWatch`'s documented no-op), so an excluded kind\n // costs zero daemon watches rather than watching and then discarding the push.\n const target = LIVE_REFRESH_KINDS.has(viewerKind) ? resolveWorkspacePath(path, cwd) : null;\n const { changedAt } = useFileWatch(target);\n\n useEffect(() => {\n if (changedAt === null) return;\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileRead(path) });\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileDownload(path) });\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileDiffByPath(path) });\n }, [changedAt, path, queryClient]);\n}\n","/**\n * FilePanel — the file/diff view toggle panel (POC `buildFilePanel`/`buildDiffPanel` +\n * `loadFileContent`/`loadDiffContent`, chat.html ~line 800-900, POC_TO_APP_PLAN_UI.md §4.5).\n * `panel-registry.ts` mounts this component for BOTH `kind:\"file\"` and `kind:\"diff\"` tabs — a\n * `kind:\"file\"` tab defaults to the File view, a `kind:\"diff\"` tab defaults to the Diff view.\n * Both views stay toggleable within the same panel via local `viewMode` state (not tab-store\n * state), matching the POC's per-panel `showFile`/`showDiff` toggle.\n *\n * The File view dispatches through the modular viewer registry (`viewer-registry.ts`):\n * `detectViewerKind` picks a viewer from the path's extension, and `VIEWER_BY_KIND` resolves the\n * (lazily-loaded) component — text/markdown/image/video today, more by adding one registry entry.\n *\n * Live updates come from the daemon's filesystem watcher via `useFileLiveRefresh` (not the\n * debounced post-tool-call invalidation in `lib/connection/files-changed.ts`, which stays as the\n * fallback for files nobody has open): only `text`/`markdown`/`image` kinds are watched — a video\n * refetch would restart playback from zero, and the binary fallback fetches nothing eagerly, so\n * there is nothing to refresh. Molecule tabs are deliberately NOT covered here — they mount\n * `MoleculeViewerPanel` elsewhere, whose `MoleculeViewer` keeps its own `shouldApplyRefresh`\n * unsaved-edits gate that this generic invalidation would bypass.\n */\n\nimport { Suspense, useState } from \"react\";\nimport { clsx } from \"clsx\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileDiff } from \"@pi-studio-ui/hooks/use-file-diff.js\";\nimport { useFileLiveRefresh } from \"@pi-studio-ui/hooks/use-file-live-refresh.js\";\nimport type { Tab, FileTabData, DiffTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { DiffView } from \"./DiffView.js\";\nimport { detectViewerKind, VIEWER_BY_KIND } from \"./viewer-registry.js\";\nimport styles from \"./FilePanel.module.css\";\n\ntype ViewMode = \"file\" | \"diff\";\n\nfunction hasPath(data: Tab[\"data\"]): data is FileTabData | DiffTabData {\n return \"path\" in data;\n}\n\nfunction isDiffTabData(data: FileTabData | DiffTabData): data is DiffTabData {\n return \"staged\" in data;\n}\n\nexport interface FilePanelProps {\n tab: Tab;\n}\n\nexport function FilePanel({ tab }: FilePanelProps) {\n const isDiffTab = tab.kind === \"diff\";\n const data = tab.data;\n const path = hasPath(data) ? data.path : \"\";\n const staged = hasPath(data) && isDiffTabData(data) ? data.staged : false;\n\n const [viewMode, setViewMode] = useState<ViewMode>(isDiffTab ? \"diff\" : \"file\");\n const cwd = tab.workspaceCwd || \"~\";\n const viewerKind = detectViewerKind(path);\n useFileLiveRefresh(path, cwd, viewerKind);\n\n const diffQuery = useFileDiff(path, cwd, staged, viewMode === \"diff\");\n\n const Viewer = VIEWER_BY_KIND[viewerKind];\n\n return (\n <Panel>\n <div className={styles.header}>\n <span className={styles.path}>{path}</span>\n <span className={styles.size}>\n {viewMode === \"diff\" ? (staged ? \"staged\" : \"unstaged\") : null}\n </span>\n <div className={styles.viewToggle}>\n <Button\n size=\"xs\"\n variant={viewMode === \"file\" ? \"default\" : \"ghost\"}\n onClick={() => setViewMode(\"file\")}\n >\n File\n </Button>\n <Button\n size=\"xs\"\n variant={viewMode === \"diff\" ? \"default\" : \"ghost\"}\n onClick={() => setViewMode(\"diff\")}\n >\n Diff\n </Button>\n </div>\n </div>\n <div className={clsx(styles.body, \"file-body\")}>\n {viewMode === \"file\" ? (\n <Suspense\n fallback={\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n }\n >\n <Viewer path={path} />\n </Suspense>\n ) : diffQuery.isLoading ? (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading diff...\n </EmptyState>\n ) : diffQuery.isError ? (\n <EmptyState>\n Error: {diffQuery.error instanceof Error ? diffQuery.error.message : \"unknown error\"}\n </EmptyState>\n ) : diffQuery.data?.patch ? (\n <DiffView patch={diffQuery.data.patch} />\n ) : (\n <EmptyState style={{ padding: \"var(--pi-spacing-30)\" }}>\n No changes (file matches HEAD)\n </EmptyState>\n )}\n </div>\n </Panel>\n );\n}\n"],"names":["useFileDiff","path","cwd","staged","enabled","client","useConnectionStore","s","useQuery","rpcKeys","LIVE_REFRESH_KINDS","useFileLiveRefresh","viewerKind","queryClient","useQueryClient","target","resolveWorkspacePath","changedAt","useFileWatch","useEffect","hasPath","data","isDiffTabData","FilePanel","tab","isDiffTab","viewMode","setViewMode","useState","detectViewerKind","diffQuery","Viewer","VIEWER_BY_KIND","Panel","jsxs","styles","jsx","Button","clsx","Suspense","EmptyState","Spinner","_a","DiffView"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FilePanel-jN6FYiGo.js","sources":["../../../src/hooks/use-file-diff.ts","../../../src/hooks/use-file-live-refresh.ts","../../../src/features/files/FilePanel.tsx"],"sourcesContent":["/**\n * `use-file-diff` — TanStack Query wrapper over `file_diff_request` (POC `loadDiffContent`,\n * POC_TO_APP_PLAN_UI.md §4.5 / §3 `hooks/use-file-diff.ts`).\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface FileDiffResult {\n patch: string;\n}\n\ninterface FileDiffRpcResponse {\n patch?: string;\n}\n\nexport function useFileDiff(path: string, cwd: string, staged: boolean, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.fileDiff(path, cwd, staged),\n queryFn: async (): Promise<FileDiffResult> => {\n if (!client) throw new Error(\"not connected\");\n const response = await client.connection.request<FileDiffRpcResponse>(\"file_diff_request\", {\n path,\n cwd,\n staged,\n });\n return { patch: response.patch ?? \"\" };\n },\n enabled: Boolean(client) && Boolean(path) && Boolean(cwd) && enabled,\n });\n}\n","/**\n * `use-file-live-refresh` — watches a file tab's OWN file (via `useFileWatch`/`watchFile`) and\n * invalidates that path's content queries on every `file_changed` push, so a `kind:\"file\"` or\n * `kind:\"diff\"` tab refetches from disk within ~1s of an external change — an editor, a shell\n * command, a build step, or `git` run in a terminal — not just a Pi tool call completing\n * (`lib/connection/files-changed.ts`'s existing debounced post-tool-call invalidation, which stays\n * as the belt-and-braces path for files nobody has open).\n *\n * `LIVE_REFRESH_KINDS` is the scope gate: only `\"text\"`, `\"markdown\"`, and `\"image\"` tabs watch.\n * `\"video\"` is excluded because a refetch mints a new object URL, which restarts playback from\n * zero mid-watch; `\"binary\"` fetches nothing at all until the user clicks its Download button, so\n * there is nothing to refresh. Exported so the gate itself is unit-testable — a new `ViewerKind`\n * must make an explicit choice here, not inherit one.\n */\n\nimport { useEffect } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { resolveWorkspacePath } from \"@pi-studio-ui/lib/paths.js\";\nimport type { ViewerKind } from \"@pi-studio-ui/features/files/viewer-registry.js\";\nimport { useFileWatch } from \"./use-file-watch.js\";\n\nexport const LIVE_REFRESH_KINDS: ReadonlySet<ViewerKind> = new Set([\"text\", \"markdown\", \"image\"]);\n\n/**\n * Invalidates `path`'s `fileRead`/`fileDownload`/`fileDiffByPath` queries on every `file_changed`\n * push for the WATCHED (resolved absolute) target — these differ for a diff tab, which is why\n * both `path` and `cwd` are taken separately. All three keys are invalidated unconditionally: a\n * watched tab can be showing any of them (`fileRead` → `TextViewer` tier 1 + `MarkdownFileViewer`;\n * `fileDownload` → `ImageViewer` and `TextViewer`'s tier-2 streamed source; `fileDiffByPath` →\n * `DiffView`, reachable from both tab kinds through `FilePanel`'s File/Diff toggle). Invalidating a\n * key with no live query is a no-op, so only *whether to watch* branches on `viewerKind`, never\n * the invalidation list.\n *\n * No client-side debounce: the daemon already coalesces at `FILE_WATCH_COALESCE_MS` (150ms). A\n * deleted file pushes too — the refetch then fails and the viewer shows its existing error state,\n * which is the correct observable outcome; no special-casing.\n */\nexport function useFileLiveRefresh(path: string, cwd: string, viewerKind: ViewerKind): void {\n const queryClient = useQueryClient();\n // A `null` target subscribes nothing (`useFileWatch`'s documented no-op), so an excluded kind\n // costs zero daemon watches rather than watching and then discarding the push.\n const target = LIVE_REFRESH_KINDS.has(viewerKind) ? resolveWorkspacePath(path, cwd) : null;\n const { changedAt } = useFileWatch(target);\n\n useEffect(() => {\n if (changedAt === null) return;\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileRead(path) });\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileDownload(path) });\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileDiffByPath(path) });\n }, [changedAt, path, queryClient]);\n}\n","/**\n * FilePanel — the file/diff view toggle panel (POC `buildFilePanel`/`buildDiffPanel` +\n * `loadFileContent`/`loadDiffContent`, chat.html ~line 800-900, POC_TO_APP_PLAN_UI.md §4.5).\n * `panel-registry.ts` mounts this component for BOTH `kind:\"file\"` and `kind:\"diff\"` tabs — a\n * `kind:\"file\"` tab defaults to the File view, a `kind:\"diff\"` tab defaults to the Diff view.\n * Both views stay toggleable within the same panel via local `viewMode` state (not tab-store\n * state), matching the POC's per-panel `showFile`/`showDiff` toggle.\n *\n * The File view dispatches through the modular viewer registry (`viewer-registry.ts`):\n * `detectViewerKind` picks a viewer from the path's extension, and `VIEWER_BY_KIND` resolves the\n * (lazily-loaded) component — text/markdown/image/video today, more by adding one registry entry.\n *\n * Live updates come from the daemon's filesystem watcher via `useFileLiveRefresh` (not the\n * debounced post-tool-call invalidation in `lib/connection/files-changed.ts`, which stays as the\n * fallback for files nobody has open): only `text`/`markdown`/`image` kinds are watched — a video\n * refetch would restart playback from zero, and the binary fallback fetches nothing eagerly, so\n * there is nothing to refresh. Molecule tabs are deliberately NOT covered here — they mount\n * `MoleculeViewerPanel` elsewhere, whose `MoleculeViewer` keeps its own `shouldApplyRefresh`\n * unsaved-edits gate that this generic invalidation would bypass.\n */\n\nimport { Suspense, useState } from \"react\";\nimport { clsx } from \"clsx\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileDiff } from \"@pi-studio-ui/hooks/use-file-diff.js\";\nimport { useFileLiveRefresh } from \"@pi-studio-ui/hooks/use-file-live-refresh.js\";\nimport type { Tab, FileTabData, DiffTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { DiffView } from \"./DiffView.js\";\nimport { detectViewerKind, VIEWER_BY_KIND } from \"./viewer-registry.js\";\nimport styles from \"./FilePanel.module.css\";\n\ntype ViewMode = \"file\" | \"diff\";\n\nfunction hasPath(data: Tab[\"data\"]): data is FileTabData | DiffTabData {\n return \"path\" in data;\n}\n\nfunction isDiffTabData(data: FileTabData | DiffTabData): data is DiffTabData {\n return \"staged\" in data;\n}\n\nexport interface FilePanelProps {\n tab: Tab;\n}\n\nexport function FilePanel({ tab }: FilePanelProps) {\n const isDiffTab = tab.kind === \"diff\";\n const data = tab.data;\n const path = hasPath(data) ? data.path : \"\";\n const staged = hasPath(data) && isDiffTabData(data) ? data.staged : false;\n\n const [viewMode, setViewMode] = useState<ViewMode>(isDiffTab ? \"diff\" : \"file\");\n const cwd = tab.workspaceCwd || \"~\";\n const viewerKind = detectViewerKind(path);\n useFileLiveRefresh(path, cwd, viewerKind);\n\n const diffQuery = useFileDiff(path, cwd, staged, viewMode === \"diff\");\n\n const Viewer = VIEWER_BY_KIND[viewerKind];\n\n return (\n <Panel>\n <div className={styles.header}>\n <span className={styles.path}>{path}</span>\n <span className={styles.size}>\n {viewMode === \"diff\" ? (staged ? \"staged\" : \"unstaged\") : null}\n </span>\n <div className={styles.viewToggle}>\n <Button\n size=\"xs\"\n variant={viewMode === \"file\" ? \"default\" : \"ghost\"}\n onClick={() => setViewMode(\"file\")}\n >\n File\n </Button>\n <Button\n size=\"xs\"\n variant={viewMode === \"diff\" ? \"default\" : \"ghost\"}\n onClick={() => setViewMode(\"diff\")}\n >\n Diff\n </Button>\n </div>\n </div>\n <div className={clsx(styles.body, \"file-body\")}>\n {viewMode === \"file\" ? (\n <Suspense\n fallback={\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n }\n >\n <Viewer path={path} />\n </Suspense>\n ) : diffQuery.isLoading ? (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading diff...\n </EmptyState>\n ) : diffQuery.isError ? (\n <EmptyState>\n Error: {diffQuery.error instanceof Error ? diffQuery.error.message : \"unknown error\"}\n </EmptyState>\n ) : diffQuery.data?.patch ? (\n <DiffView patch={diffQuery.data.patch} />\n ) : (\n <EmptyState style={{ padding: \"var(--pi-spacing-30)\" }}>\n No changes (file matches HEAD)\n </EmptyState>\n )}\n </div>\n </Panel>\n );\n}\n"],"names":["useFileDiff","path","cwd","staged","enabled","client","useConnectionStore","s","useQuery","rpcKeys","LIVE_REFRESH_KINDS","useFileLiveRefresh","viewerKind","queryClient","useQueryClient","target","resolveWorkspacePath","changedAt","useFileWatch","useEffect","hasPath","data","isDiffTabData","FilePanel","tab","isDiffTab","viewMode","setViewMode","useState","detectViewerKind","diffQuery","Viewer","VIEWER_BY_KIND","Panel","jsxs","styles","jsx","Button","clsx","Suspense","EmptyState","Spinner","_a","DiffView"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as o}from"./vendor-react-CWo7Do4s.js";import{E as e,S as t}from"./index-
|
|
2
|
-
//# sourceMappingURL=ImageViewer-
|
|
1
|
+
import{j as o}from"./vendor-react-CWo7Do4s.js";import{E as e,S as t}from"./index-KQwi4ZXj.js";import{u as a}from"./use-file-download-Dv5HRkkB.js";import"./vendor-query-BGIh-vlp.js";import"./vendor-04vXFIXe.js";import"./vendor-markdown-YvkQLm_s.js";import"./vendor-highlight-Efc6A-g6.js";import"./vendor-icons-CJ_dOXcR.js";import"./vendor-overlays-BnmaUbKw.js";import"./vendor-dnd-Bj2_GhfD.js";const m="_wrap_1vzo5_1",n="_image_1vzo5_15",s={wrap:m,image:n};function E({path:i}){const r=a(i);return r.isLoading?o.jsxs(e,{children:[o.jsx(t,{size:"sm"})," Loading image..."]}):r.isError?o.jsxs(e,{children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?o.jsx("div",{className:s.wrap,children:o.jsx("img",{className:s.image,src:r.data.objectUrl,alt:i.split("/").pop()||i})}):null}export{E as ImageViewer};
|
|
2
|
+
//# sourceMappingURL=ImageViewer-DdvsjbtG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageViewer-
|
|
1
|
+
{"version":3,"file":"ImageViewer-DdvsjbtG.js","sources":["../../../src/features/files/ImageViewer.tsx"],"sourcesContent":["/**\n * ImageViewer — renders a binary image file by downloading it over the file-transfer binary\n * frames (`useFileDownload`) and displaying the resulting object URL (POC_TO_APP_PLAN_UI.md §4.5\n * follow-up: modular file preview). A checkerboard background shows through transparent PNGs.\n */\n\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport styles from \"./ImageViewer.module.css\";\n\nexport function ImageViewer({ path }: ViewerProps) {\n const query = useFileDownload(path);\n\n if (query.isLoading) {\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading image...\n </EmptyState>\n );\n }\n if (query.isError) {\n return (\n <EmptyState>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </EmptyState>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n <img\n className={styles.image}\n src={query.data.objectUrl}\n alt={path.split(\"/\").pop() || path}\n />\n </div>\n );\n}\n"],"names":["ImageViewer","path","query","useFileDownload","EmptyState","jsx","Spinner","styles"],"mappings":"wcAYO,SAASA,EAAY,CAAE,KAAAC,GAAqB,CACjD,MAAMC,EAAQC,EAAgBF,CAAI,EAElC,OAAIC,EAAM,iBAELE,EAAA,CACC,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,mBAAA,EACvB,EAGAJ,EAAM,eAELE,EAAA,CAAW,SAAA,CAAA,UACFF,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,KAGTG,EAAAA,IAAC,MAAA,CAAI,UAAWE,EAAO,KACrB,SAAAF,EAAAA,IAAC,MAAA,CACC,UAAWE,EAAO,MAClB,IAAKL,EAAM,KAAK,UAChB,IAAKD,EAAK,MAAM,GAAG,EAAE,OAASA,CAAA,CAAA,EAElC,EATsB,IAW1B"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as d,j as r}from"./vendor-react-CWo7Do4s.js";import{E as n,S as c,B as a}from"./index-
|
|
2
|
-
//# sourceMappingURL=MarkdownFileViewer-
|
|
1
|
+
import{r as d,j as r}from"./vendor-react-CWo7Do4s.js";import{E as n,S as c,B as a}from"./index-KQwi4ZXj.js";import{u as l,C as m}from"./CodeView-CXE9vlF1.js";import{M as p}from"./markdown-CfR91Pqp.js";import"./vendor-query-BGIh-vlp.js";import"./vendor-04vXFIXe.js";import"./vendor-markdown-YvkQLm_s.js";import"./vendor-highlight-Efc6A-g6.js";import"./vendor-icons-CJ_dOXcR.js";import"./vendor-overlays-BnmaUbKw.js";import"./vendor-dnd-Bj2_GhfD.js";const u="_wrap_qjlik_1",x="_toolbar_qjlik_6",j="_rendered_qjlik_17",s={wrap:u,toolbar:x,rendered:j};function C({path:t}){const e=l(t),[o,i]=d.useState("preview");return e.isLoading?r.jsxs(n,{children:[r.jsx(c,{size:"sm"})," Loading..."]}):e.isError?r.jsxs(n,{children:["Error: ",e.error instanceof Error?e.error.message:"unknown error"]}):e.data?r.jsxs("div",{className:s.wrap,children:[r.jsxs("div",{className:s.toolbar,children:[r.jsx(a,{size:"xs",variant:o==="preview"?"default":"ghost",onClick:()=>i("preview"),children:"Preview"}),r.jsx(a,{size:"xs",variant:o==="source"?"default":"ghost",onClick:()=>i("source"),children:"Source"})]}),o==="preview"?r.jsx("div",{className:s.rendered,children:r.jsx(p,{text:e.data.content})}):r.jsx(m,{path:t,content:e.data.content})]}):null}export{C as MarkdownFileViewer};
|
|
2
|
+
//# sourceMappingURL=MarkdownFileViewer-BNG2in_C.js.map
|
package/dist/web/assets/{MarkdownFileViewer-C2QKqSk4.js.map → MarkdownFileViewer-BNG2in_C.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkdownFileViewer-
|
|
1
|
+
{"version":3,"file":"MarkdownFileViewer-BNG2in_C.js","sources":["../../../src/features/files/MarkdownFileViewer.tsx"],"sourcesContent":["/**\n * MarkdownFileViewer — renders a `.md`/`.mdx` file's content through the app's sanitized\n * Markdown renderer (`timeline/markdown.tsx`, react-markdown + remark-gfm — no\n * `dangerouslySetInnerHTML` of user content), with a Preview/Source toggle since raw markdown is\n * sometimes what you actually want to read (POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular file\n * preview).\n */\n\nimport { useState } from \"react\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileRead } from \"@pi-studio-ui/hooks/use-file-read.js\";\nimport { Markdown } from \"@pi-studio-ui/timeline/markdown.js\";\nimport { CodeView } from \"./CodeView.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport styles from \"./MarkdownFileViewer.module.css\";\n\ntype Mode = \"preview\" | \"source\";\n\nexport function MarkdownFileViewer({ path }: ViewerProps) {\n const query = useFileRead(path);\n const [mode, setMode] = useState<Mode>(\"preview\");\n\n if (query.isLoading) {\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n );\n }\n if (query.isError) {\n return (\n <EmptyState>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </EmptyState>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n <div className={styles.toolbar}>\n <Button\n size=\"xs\"\n variant={mode === \"preview\" ? \"default\" : \"ghost\"}\n onClick={() => setMode(\"preview\")}\n >\n Preview\n </Button>\n <Button\n size=\"xs\"\n variant={mode === \"source\" ? \"default\" : \"ghost\"}\n onClick={() => setMode(\"source\")}\n >\n Source\n </Button>\n </div>\n {mode === \"preview\" ? (\n <div className={styles.rendered}>\n <Markdown text={query.data.content} />\n </div>\n ) : (\n <CodeView path={path} content={query.data.content} />\n )}\n </div>\n );\n}\n"],"names":["MarkdownFileViewer","path","query","useFileRead","mode","setMode","useState","EmptyState","jsx","Spinner","jsxs","styles","Button","Markdown","CodeView"],"mappings":"oiBAoBO,SAASA,EAAmB,CAAE,KAAAC,GAAqB,CACxD,MAAMC,EAAQC,EAAYF,CAAI,EACxB,CAACG,EAAMC,CAAO,EAAIC,EAAAA,SAAe,SAAS,EAEhD,OAAIJ,EAAM,iBAELK,EAAA,CACC,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,EACvB,EAGAP,EAAM,eAELK,EAAA,CAAW,SAAA,CAAA,UACFL,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,KAGTQ,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,KACrB,SAAA,CAAAD,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,QACrB,SAAA,CAAAH,EAAAA,IAACI,EAAA,CACC,KAAK,KACL,QAASR,IAAS,UAAY,UAAY,QAC1C,QAAS,IAAMC,EAAQ,SAAS,EACjC,SAAA,SAAA,CAAA,EAGDG,EAAAA,IAACI,EAAA,CACC,KAAK,KACL,QAASR,IAAS,SAAW,UAAY,QACzC,QAAS,IAAMC,EAAQ,QAAQ,EAChC,SAAA,QAAA,CAAA,CAED,EACF,EACCD,IAAS,UACRI,MAAC,MAAA,CAAI,UAAWG,EAAO,SACrB,eAACE,EAAA,CAAS,KAAMX,EAAM,KAAK,OAAA,CAAS,EACtC,EAEAM,EAAAA,IAACM,GAAS,KAAAb,EAAY,QAASC,EAAM,KAAK,OAAA,CAAS,CAAA,EAEvD,EA3BsB,IA6B1B"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as n,j as r}from"./vendor-react-CWo7Do4s.js";import{a as V,P as c,E as _,S as A,h as F}from"./index-
|
|
2
|
-
//# sourceMappingURL=MoleculeViewerPanel-
|
|
1
|
+
import{r as n,j as r}from"./vendor-react-CWo7Do4s.js";import{a as V,P as c,E as _,S as A,h as F}from"./index-KQwi4ZXj.js";import{m as L}from"./vendor-molviewer-DE5Dt5UM.js";import{S}from"./StatusBadge-DFSeaZ3p.js";import{u as N}from"./use-file-download-Dv5HRkkB.js";import{u as T}from"./use-file-watch-DTOtoP8W.js";import"./vendor-query-BGIh-vlp.js";import"./vendor-04vXFIXe.js";import"./vendor-markdown-YvkQLm_s.js";import"./vendor-highlight-Efc6A-g6.js";import"./vendor-icons-CJ_dOXcR.js";import"./vendor-overlays-BnmaUbKw.js";import"./vendor-dnd-Bj2_GhfD.js";function P(e,o){return!e||!o?null:{url:o,name:e.split("/").pop()||e}}function B({changedAt:e,lastAppliedAt:o,modified:t}){return!(e===null||t||e===o)}const k={empty_path:"Nothing to save.",too_large:"File is too large to save (over 5 MiB).",not_found:"The file no longer exists.",unreadable:"The file could not be read.",not_a_file:"That path is not a file."};async function q(e,o,t){const l=await e.connection.request("file_write_request",{path:o,content:t});if(!l.ok){const s=l.error??"";throw new Error(k[s]??s??"Failed to save")}}const I={"--canvas":"var(--pi-color-surface0)","--well":"var(--pi-color-surface0)","--recess":"var(--pi-color-surface1)","--chrome":"var(--pi-color-surface1)","--control":"var(--pi-color-surface2)","--control-hover":"var(--pi-color-surface3)","--control-strong-hover":"var(--pi-color-surface4)","--border-control":"var(--pi-color-border)","--border-input":"var(--pi-color-border)","--popover":"var(--pi-color-surface1)","--popover-item-hover":"var(--pi-color-surface2)","--row-hover":"var(--pi-color-surface2)","--border-popover":"var(--pi-color-border)","--text-primary":"var(--pi-color-foreground)","--text-on-control":"var(--pi-color-foreground)"},O="_wrap_tixu9_1",W="_molViewer_tixu9_4",z="_badges_tixu9_8",m={wrap:O,molViewer:W,badges:z};function D({path:e,isActive:o,onModifiedChange:t}){var E;const l=V(a=>a.client),s=N(e??"",!!e),{changedAt:i}=T(e),b=n.useRef(null),v=n.useRef(!1),[u,w]=n.useState(null),[d,j]=n.useState(!1),[x,f]=n.useState(null),p=n.useRef(null),R=P(e,(E=s.data)==null?void 0:E.objectUrl);n.useEffect(()=>{B({changedAt:i,lastAppliedAt:p.current,modified:d})&&(p.current=i,s.refetch())},[i,d,s.refetch]);const y=i!==null&&i!==p.current&&d;async function M(a){if(e){if(!l){f("Not connected — cannot save.");return}f(null);try{await q(l,e,a.text()),a.saved()}catch(g){f(g instanceof Error?g.message:"Failed to save")}}}const h=(s.isError?s.error instanceof Error?s.error.message:"download failed":null)??(u==null?void 0:u.message)??null;return e&&s.isPending?r.jsx(c,{children:r.jsxs(_,{children:[r.jsx(A,{size:"sm"})," Loading..."]})}):h?r.jsx(c,{children:r.jsxs(_,{children:["Error: ",h]})}):r.jsxs(c,{className:m.wrap,"data-molecule-active":o?"true":"false",children:[r.jsxs("div",{className:m.badges,children:[y&&r.jsx(S,{label:"File changed on disk",variant:"muted"}),x&&r.jsx(S,{label:`Save failed: ${x}`,variant:"error"})]}),r.jsx(L,{ref:b,className:m.molViewer,theme:I,source:R,sourceMode:v.current?"update":"replace",onLoad:()=>{v.current=!0,w(null)},onLoadError:a=>w(a.error),onModifiedChange:a=>{j(a),t==null||t(a)},onSave:e?M:void 0})]})}function se({tab:e}){const{path:o}=e.data,t=F(e.id);return r.jsx(c,{children:r.jsx(D,{path:o,isActive:t})})}export{se as MoleculeViewerPanel};
|
|
2
|
+
//# sourceMappingURL=MoleculeViewerPanel-CQQ9CIsD.js.map
|
package/dist/web/assets/{MoleculeViewerPanel-v4ED-TPp.js.map → MoleculeViewerPanel-CQQ9CIsD.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MoleculeViewerPanel-v4ED-TPp.js","sources":["../../../src/features/files/molecule-source.ts","../../../src/features/files/molecule-reload.ts","../../../src/features/files/write-file.ts","../../../src/features/files/molecule-theme.ts","../../../src/features/files/MoleculeViewer.tsx","../../../src/features/files/MoleculeViewerPanel.tsx"],"sourcesContent":["/**\n * Pure derivation of molviewer's `{ url, name }` source shape, kept in its own module (no\n * `@molviewer/core` import) so it is unit-testable without pulling in the WebGL viewer bundle —\n * merely importing `@molviewer/core` touches `document` at module scope, which jsdom-less/node\n * test environments don't have.\n */\nimport type { MolViewerSource } from \"@molviewer/core\";\n\n/** `name` is load-bearing: molviewer resolves the file format from its extension. */\nexport function moleculeSource(\n path: string | null,\n objectUrl: string | null | undefined,\n): MolViewerSource | null {\n if (!path || !objectUrl) return null;\n return { url: objectUrl, name: path.split(\"/\").pop() || path };\n}\n","/**\n * Pure reload-gating decision for the molecule viewer's live reload (task-006/007). Kept in its\n * own module — like `molecule-source.ts` — so it is unit-testable without importing\n * `@molviewer/core` (which touches `document` at module scope).\n *\n * The gate is intentionally just \"are there unsaved in-viewer edits right now\" — no timestamp or\n * echo-suppression window. A future in-viewer \"Save\" cannot self-trigger a clobber: a save can\n * only happen while `modified` is `true`, and the earliest it flips back to `false` is after the\n * write completes — at which point a reload triggered by that same write is a content no-op.\n */\nexport interface ShouldApplyRefreshArgs {\n /** Timestamp of the last `file_changed` push for this path, or `null` if none yet. */\n changedAt: number | null;\n /** Timestamp of the last push this component actually reloaded for, or `null`. */\n lastAppliedAt: number | null;\n /** Whether the viewer currently has unsaved in-viewer edits. */\n modified: boolean;\n}\n\nexport function shouldApplyRefresh({\n changedAt,\n lastAppliedAt,\n modified,\n}: ShouldApplyRefreshArgs): boolean {\n if (changedAt === null) return false; // No change pushed yet.\n if (modified) return false; // Unsaved edits — leave the user's work alone.\n if (changedAt === lastAppliedAt) return false; // Already applied this push — no reload loop.\n return true;\n}\n","/**\n * Shared `file_write_request` caller for save flows (currently the molecule viewer's `onSave`,\n * `MoleculeViewer.tsx`) — one place for the wire call and the server error-code → user-facing\n * message mapping, structured the same way as the sibling `move-entry.ts`. Overwrite-only: the\n * daemon 404s `not_found` for a missing target (`FileExplorerService.writeFile`); there is no\n * create-on-write.\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\nconst ERROR_MESSAGES: Record<string, string> = {\n empty_path: \"Nothing to save.\",\n too_large: \"File is too large to save (over 5 MiB).\",\n not_found: \"The file no longer exists.\",\n unreadable: \"The file could not be read.\",\n not_a_file: \"That path is not a file.\",\n};\n\n/** Overwrite `path`'s content on disk. Throws an Error carrying a user-facing message on\n * failure. */\nexport async function writeFile(\n client: PiStudioClient,\n path: string,\n content: string,\n): Promise<void> {\n const response = await client.connection.request<{ ok: boolean; error?: string }>(\n \"file_write_request\",\n { path, content },\n );\n if (!response.ok) {\n const code = response.error ?? \"\";\n throw new Error(ERROR_MESSAGES[code] ?? code ?? \"Failed to save\");\n }\n}\n","/**\n * MOLVIEWER_THEME — pi-studio's background/foreground override for `<MolViewer>`'s `theme` prop.\n *\n * `theme` is documented as raw CSS custom properties applied inline to the component's root\n * (`docs/molviewer-core-doc.md` § UI composition) — a completely separate knob from\n * `initialView.backgroundColor` (the WebGL scene's clear color) and `colorScheme` (per-atom\n * coloring). Neither of those is touched here; this only changes chrome DOM elements (sidebar,\n * toolbars, panels, buttons, dropdowns), never the rendered molecule itself.\n *\n * Values are `var(--pi-color-*)` references, not resolved hex — a CSS custom property may hold\n * another `var()` reference and it chains through the cascade like any other value, so\n * molviewer's chrome tracks pi-studio's live theme (light/dark, custom accent) automatically,\n * with no React re-render needed when appearance settings change.\n *\n * Scope is deliberately narrow, per product decision: panel backgrounds, primary text, and —\n * where molviewer exposes an equivalent — the surfaces of its own interactive controls (buttons)\n * and popovers (dropdowns/menus), all mapped onto pi-studio's existing surface/hover/border scale\n * (`Button.module.css`, `Select.module.css`, the `.item[data-highlighted]` hover pattern used\n * across every pi-studio dropdown). Molviewer's semantic/interactive colors that pi-studio has no\n * equivalent for — accent, selection, danger/success/warning, its five-step muted text scale,\n * axis colors — are its own design language and are left untouched.\n *\n * Molviewer's shipped defaults, for reference (`@molviewer/core/dist/style.css`'s `:root` block):\n * --canvas #0a0c10 --well #0e1116 --recess #0f1217 --chrome #12151b\n * --control #1e232c --control-hover #262c37 --control-strong-hover #2b323e\n * --popover #171b22 --popover-item-hover #232935 --row-hover #161a21\n * --border-control #333b48 --border-input #2b323e --border-popover #3b4453\n * --text-primary #e6e9ef --text-on-control #dfe5ee\n */\nexport const MOLVIEWER_THEME: Record<string, string> = {\n // Panel backgrounds (sidebar, toolbars, recessed panels).\n \"--canvas\": \"var(--pi-color-surface0)\",\n \"--well\": \"var(--pi-color-surface0)\",\n \"--recess\": \"var(--pi-color-surface1)\",\n \"--chrome\": \"var(--pi-color-surface1)\",\n // Buttons and other controls.\n \"--control\": \"var(--pi-color-surface2)\",\n \"--control-hover\": \"var(--pi-color-surface3)\",\n \"--control-strong-hover\": \"var(--pi-color-surface4)\",\n \"--border-control\": \"var(--pi-color-border)\",\n \"--border-input\": \"var(--pi-color-border)\",\n // Dropdowns / menus.\n \"--popover\": \"var(--pi-color-surface1)\",\n \"--popover-item-hover\": \"var(--pi-color-surface2)\",\n \"--row-hover\": \"var(--pi-color-surface2)\",\n \"--border-popover\": \"var(--pi-color-border)\",\n // Text.\n \"--text-primary\": \"var(--pi-color-foreground)\",\n \"--text-on-control\": \"var(--pi-color-foreground)\",\n};\n","/**\n * MoleculeViewer — mounts `<MolViewer>` for both molecule-tab shapes: a file-backed tab (fetch\n * the file, hand molviewer the bytes) and an empty (\"+\"-menu) tab (no source — molviewer's own\n * drag-drop empty state, `ui.emptyState`'s `FirstRunCard`).\n *\n * Content is fetched through `useFileDownload` (chunked binary transfer, uncapped), not\n * `useFileRead` (`file_read_request`, UTF-8 text, inline-size capped): molviewer wants raw bytes\n * and the object URL that hook already produces is exactly molviewer's `{ url, name }` source\n * shape — no decode step, no new fetch hook.\n *\n * Live reload (task-006/007): `useFileWatch` pushes a new `changedAt` whenever the daemon detects\n * the file changed on disk. `shouldApplyRefresh` (kept in its own pure module, see\n * `molecule-reload.ts`) gates the reaction on there being no unsaved in-viewer edits; when it\n * clears, `download.refetch()` mints a fresh object URL, the `source` prop changes, and\n * `MolViewer` reloads with `sourceMode=\"update\"` — camera/selection survive. When it's gated by\n * unsaved edits, a small stale-file indicator surfaces instead of silently diverging.\n *\n * Save (`onSave`, `@molviewer/core@0.4.0`): only wired when `path` is non-null — a file-backed\n * tab writes the viewer's current serialization straight back to that same absolute path via\n * `file_write_request` (`write-file.ts`); the empty (\"+\"-menu) tab has nowhere to write and gets\n * no Save button at all (passing `onSave` is what draws it). `e.saved()` is called only after the\n * daemon confirms the write, which is what greys the button out and flips the viewer back to\n * clean — a failed write leaves it live so the user can retry. Note `e.fileName` is molviewer's\n * own load-time basename (`moleculeSource`'s `name`, extension only, no directory), not a usable\n * write target — the absolute `path` prop already in scope is what gets written.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { MolViewer, type MolViewerHandle, type SaveEvent } from \"@molviewer/core\";\nimport \"@molviewer/core/style.css\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { StatusBadge } from \"@pi-studio-ui/components/primitives/StatusBadge.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { useFileWatch } from \"@pi-studio-ui/hooks/use-file-watch.js\";\nimport { moleculeSource } from \"./molecule-source.js\";\nimport { shouldApplyRefresh } from \"./molecule-reload.js\";\nimport { writeFile } from \"./write-file.js\";\nimport { MOLVIEWER_THEME } from \"./molecule-theme.js\";\nimport styles from \"./MoleculeViewer.module.css\";\n\nexport interface MoleculeViewerProps {\n /** Absolute path of the file to render, or null for an empty (\"New molecule view\") tab. */\n path: string | null;\n /** True when this viewer's tab is the visible one. Molstar's own `ResizeObserver` on its canvas\n * container re-fits on becoming visible (verified in the installed bundle), so this is not\n * currently used to drive a manual re-fit — kept as the escape hatch task-010's visual\n * verification may still need. */\n isActive?: boolean;\n /** Mirrors `MolViewerProps.onModifiedChange` upward for a future parent-level consumer (e.g. a\n * tab-strip \"unsaved changes\" mark) — independent of this component's own internal `modified`\n * state below, which drives the live-reload gate. */\n onModifiedChange?: (modified: boolean) => void;\n}\n\nexport function MoleculeViewer({ path, isActive, onModifiedChange }: MoleculeViewerProps) {\n const client = useConnectionStore((s) => s.client);\n const download = useFileDownload(path ?? \"\", Boolean(path));\n const { changedAt } = useFileWatch(path);\n const handleRef = useRef<MolViewerHandle>(null);\n // First load of this tab's file refits the camera (\"replace\"); every reload after that (the\n // same file changing on disk, task-007) preserves camera/selection/undo (\"update\"). A mounted\n // MoleculeViewer corresponds to exactly one tab for its whole lifetime (task-004 mints a new\n // tab id per path), so this never needs to reset for a \"new\" path mid-lifetime.\n const hasLoadedRef = useRef(false);\n const [loadError, setLoadError] = useState<Error | null>(null);\n const [modified, setModified] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n // Ref, not state: recording \"which push we last acted on\" doesn't need its own render — it's\n // read by the same effect that writes it, and by the stale-indicator check below.\n const lastAppliedAtRef = useRef<number | null>(null);\n\n const source = moleculeSource(path, download.data?.objectUrl);\n\n useEffect(() => {\n if (!shouldApplyRefresh({ changedAt, lastAppliedAt: lastAppliedAtRef.current, modified })) {\n return;\n }\n lastAppliedAtRef.current = changedAt;\n void download.refetch();\n // Only `download.refetch` itself needs to be stable across renders for this effect to behave\n // correctly; depending on the whole `download` query object would re-run on every unrelated\n // status change it produces.\n }, [changedAt, modified, download.refetch]);\n\n // A push arrived but couldn't be applied because of unsaved edits — surface that rather than\n // silently diverging. Reading the ref during render is safe here: it only ever changes inside\n // the effect above, which always runs before the next paint that could observe a stale value.\n const hasUnappliedChange =\n changedAt !== null && changedAt !== lastAppliedAtRef.current && modified;\n\n async function handleSave(e: SaveEvent) {\n if (!path) return;\n if (!client) {\n setSaveError(\"Not connected — cannot save.\");\n return;\n }\n setSaveError(null);\n try {\n await writeFile(client, path, e.text());\n e.saved();\n } catch (err) {\n setSaveError(err instanceof Error ? err.message : \"Failed to save\");\n }\n }\n\n const downloadErrorMessage = download.isError\n ? download.error instanceof Error\n ? download.error.message\n : \"download failed\"\n : null;\n const errorMessage = downloadErrorMessage ?? loadError?.message ?? null;\n\n if (path && download.isPending) {\n return (\n <Panel>\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n </Panel>\n );\n }\n if (errorMessage) {\n return (\n <Panel>\n <EmptyState>Error: {errorMessage}</EmptyState>\n </Panel>\n );\n }\n\n return (\n <Panel className={styles.wrap} data-molecule-active={isActive ? \"true\" : \"false\"}>\n <div className={styles.badges}>\n {hasUnappliedChange && <StatusBadge label=\"File changed on disk\" variant=\"muted\" />}\n {saveError && <StatusBadge label={`Save failed: ${saveError}`} variant=\"error\" />}\n </div>\n <MolViewer\n ref={handleRef}\n className={styles.molViewer}\n theme={MOLVIEWER_THEME}\n source={source}\n sourceMode={hasLoadedRef.current ? \"update\" : \"replace\"}\n onLoad={() => {\n hasLoadedRef.current = true;\n setLoadError(null);\n }}\n onLoadError={(e) => setLoadError(e.error)}\n onModifiedChange={(m) => {\n setModified(m);\n onModifiedChange?.(m);\n }}\n onSave={path ? handleSave : undefined}\n />\n </Panel>\n );\n}\n","/**\n * MoleculeViewerPanel — thin `PanelProps` adapter for a `kind: \"molecule\"` tab. No header, no\n * File/Diff toggle (a molecule tab has neither — diffs come from the git Changes panel\n * independently, `ChangesPanel.tsx`); it owns the panel-filling height chain and hands off to\n * `MoleculeViewer` for everything else.\n */\n\nimport { useIsTabVisible, type MoleculeTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { PanelProps } from \"@pi-studio-ui/features/workspace/panel-registry.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { MoleculeViewer } from \"./MoleculeViewer.js\";\n\nexport function MoleculeViewerPanel({ tab }: PanelProps) {\n const { path } = tab.data as MoleculeTabData;\n // Per-pane visibility: a molecule tab in a non-focused pane is still on screen and must resize.\n const isActive = useIsTabVisible(tab.id);\n\n return (\n <Panel>\n <MoleculeViewer path={path} isActive={isActive} />\n </Panel>\n );\n}\n"],"names":["moleculeSource","path","objectUrl","shouldApplyRefresh","changedAt","lastAppliedAt","modified","ERROR_MESSAGES","writeFile","client","content","response","code","MOLVIEWER_THEME","MoleculeViewer","isActive","onModifiedChange","useConnectionStore","s","download","useFileDownload","useFileWatch","handleRef","useRef","hasLoadedRef","loadError","setLoadError","useState","setModified","saveError","setSaveError","lastAppliedAtRef","source","_a","useEffect","hasUnappliedChange","handleSave","e","err","errorMessage","jsx","Panel","jsxs","EmptyState","Spinner","styles","StatusBadge","MolViewer","m","MoleculeViewerPanel","tab","useIsTabVisible"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MoleculeViewerPanel-CQQ9CIsD.js","sources":["../../../src/features/files/molecule-source.ts","../../../src/features/files/molecule-reload.ts","../../../src/features/files/write-file.ts","../../../src/features/files/molecule-theme.ts","../../../src/features/files/MoleculeViewer.tsx","../../../src/features/files/MoleculeViewerPanel.tsx"],"sourcesContent":["/**\n * Pure derivation of molviewer's `{ url, name }` source shape, kept in its own module (no\n * `@molviewer/core` import) so it is unit-testable without pulling in the WebGL viewer bundle —\n * merely importing `@molviewer/core` touches `document` at module scope, which jsdom-less/node\n * test environments don't have.\n */\nimport type { MolViewerSource } from \"@molviewer/core\";\n\n/** `name` is load-bearing: molviewer resolves the file format from its extension. */\nexport function moleculeSource(\n path: string | null,\n objectUrl: string | null | undefined,\n): MolViewerSource | null {\n if (!path || !objectUrl) return null;\n return { url: objectUrl, name: path.split(\"/\").pop() || path };\n}\n","/**\n * Pure reload-gating decision for the molecule viewer's live reload (task-006/007). Kept in its\n * own module — like `molecule-source.ts` — so it is unit-testable without importing\n * `@molviewer/core` (which touches `document` at module scope).\n *\n * The gate is intentionally just \"are there unsaved in-viewer edits right now\" — no timestamp or\n * echo-suppression window. A future in-viewer \"Save\" cannot self-trigger a clobber: a save can\n * only happen while `modified` is `true`, and the earliest it flips back to `false` is after the\n * write completes — at which point a reload triggered by that same write is a content no-op.\n */\nexport interface ShouldApplyRefreshArgs {\n /** Timestamp of the last `file_changed` push for this path, or `null` if none yet. */\n changedAt: number | null;\n /** Timestamp of the last push this component actually reloaded for, or `null`. */\n lastAppliedAt: number | null;\n /** Whether the viewer currently has unsaved in-viewer edits. */\n modified: boolean;\n}\n\nexport function shouldApplyRefresh({\n changedAt,\n lastAppliedAt,\n modified,\n}: ShouldApplyRefreshArgs): boolean {\n if (changedAt === null) return false; // No change pushed yet.\n if (modified) return false; // Unsaved edits — leave the user's work alone.\n if (changedAt === lastAppliedAt) return false; // Already applied this push — no reload loop.\n return true;\n}\n","/**\n * Shared `file_write_request` caller for save flows (currently the molecule viewer's `onSave`,\n * `MoleculeViewer.tsx`) — one place for the wire call and the server error-code → user-facing\n * message mapping, structured the same way as the sibling `move-entry.ts`. Overwrite-only: the\n * daemon 404s `not_found` for a missing target (`FileExplorerService.writeFile`); there is no\n * create-on-write.\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\nconst ERROR_MESSAGES: Record<string, string> = {\n empty_path: \"Nothing to save.\",\n too_large: \"File is too large to save (over 5 MiB).\",\n not_found: \"The file no longer exists.\",\n unreadable: \"The file could not be read.\",\n not_a_file: \"That path is not a file.\",\n};\n\n/** Overwrite `path`'s content on disk. Throws an Error carrying a user-facing message on\n * failure. */\nexport async function writeFile(\n client: PiStudioClient,\n path: string,\n content: string,\n): Promise<void> {\n const response = await client.connection.request<{ ok: boolean; error?: string }>(\n \"file_write_request\",\n { path, content },\n );\n if (!response.ok) {\n const code = response.error ?? \"\";\n throw new Error(ERROR_MESSAGES[code] ?? code ?? \"Failed to save\");\n }\n}\n","/**\n * MOLVIEWER_THEME — pi-studio's background/foreground override for `<MolViewer>`'s `theme` prop.\n *\n * `theme` is documented as raw CSS custom properties applied inline to the component's root\n * (`docs/molviewer-core-doc.md` § UI composition) — a completely separate knob from\n * `initialView.backgroundColor` (the WebGL scene's clear color) and `colorScheme` (per-atom\n * coloring). Neither of those is touched here; this only changes chrome DOM elements (sidebar,\n * toolbars, panels, buttons, dropdowns), never the rendered molecule itself.\n *\n * Values are `var(--pi-color-*)` references, not resolved hex — a CSS custom property may hold\n * another `var()` reference and it chains through the cascade like any other value, so\n * molviewer's chrome tracks pi-studio's live theme (light/dark, custom accent) automatically,\n * with no React re-render needed when appearance settings change.\n *\n * Scope is deliberately narrow, per product decision: panel backgrounds, primary text, and —\n * where molviewer exposes an equivalent — the surfaces of its own interactive controls (buttons)\n * and popovers (dropdowns/menus), all mapped onto pi-studio's existing surface/hover/border scale\n * (`Button.module.css`, `Select.module.css`, the `.item[data-highlighted]` hover pattern used\n * across every pi-studio dropdown). Molviewer's semantic/interactive colors that pi-studio has no\n * equivalent for — accent, selection, danger/success/warning, its five-step muted text scale,\n * axis colors — are its own design language and are left untouched.\n *\n * Molviewer's shipped defaults, for reference (`@molviewer/core/dist/style.css`'s `:root` block):\n * --canvas #0a0c10 --well #0e1116 --recess #0f1217 --chrome #12151b\n * --control #1e232c --control-hover #262c37 --control-strong-hover #2b323e\n * --popover #171b22 --popover-item-hover #232935 --row-hover #161a21\n * --border-control #333b48 --border-input #2b323e --border-popover #3b4453\n * --text-primary #e6e9ef --text-on-control #dfe5ee\n */\nexport const MOLVIEWER_THEME: Record<string, string> = {\n // Panel backgrounds (sidebar, toolbars, recessed panels).\n \"--canvas\": \"var(--pi-color-surface0)\",\n \"--well\": \"var(--pi-color-surface0)\",\n \"--recess\": \"var(--pi-color-surface1)\",\n \"--chrome\": \"var(--pi-color-surface1)\",\n // Buttons and other controls.\n \"--control\": \"var(--pi-color-surface2)\",\n \"--control-hover\": \"var(--pi-color-surface3)\",\n \"--control-strong-hover\": \"var(--pi-color-surface4)\",\n \"--border-control\": \"var(--pi-color-border)\",\n \"--border-input\": \"var(--pi-color-border)\",\n // Dropdowns / menus.\n \"--popover\": \"var(--pi-color-surface1)\",\n \"--popover-item-hover\": \"var(--pi-color-surface2)\",\n \"--row-hover\": \"var(--pi-color-surface2)\",\n \"--border-popover\": \"var(--pi-color-border)\",\n // Text.\n \"--text-primary\": \"var(--pi-color-foreground)\",\n \"--text-on-control\": \"var(--pi-color-foreground)\",\n};\n","/**\n * MoleculeViewer — mounts `<MolViewer>` for both molecule-tab shapes: a file-backed tab (fetch\n * the file, hand molviewer the bytes) and an empty (\"+\"-menu) tab (no source — molviewer's own\n * drag-drop empty state, `ui.emptyState`'s `FirstRunCard`).\n *\n * Content is fetched through `useFileDownload` (chunked binary transfer, uncapped), not\n * `useFileRead` (`file_read_request`, UTF-8 text, inline-size capped): molviewer wants raw bytes\n * and the object URL that hook already produces is exactly molviewer's `{ url, name }` source\n * shape — no decode step, no new fetch hook.\n *\n * Live reload (task-006/007): `useFileWatch` pushes a new `changedAt` whenever the daemon detects\n * the file changed on disk. `shouldApplyRefresh` (kept in its own pure module, see\n * `molecule-reload.ts`) gates the reaction on there being no unsaved in-viewer edits; when it\n * clears, `download.refetch()` mints a fresh object URL, the `source` prop changes, and\n * `MolViewer` reloads with `sourceMode=\"update\"` — camera/selection survive. When it's gated by\n * unsaved edits, a small stale-file indicator surfaces instead of silently diverging.\n *\n * Save (`onSave`, `@molviewer/core@0.4.1`): only wired when `path` is non-null — a file-backed\n * tab writes the viewer's current serialization straight back to that same absolute path via\n * `file_write_request` (`write-file.ts`); the empty (\"+\"-menu) tab has nowhere to write and gets\n * no Save button at all (passing `onSave` is what draws it). `e.saved()` is called only after the\n * daemon confirms the write, which is what greys the button out and flips the viewer back to\n * clean — a failed write leaves it live so the user can retry. Note `e.fileName` is molviewer's\n * own load-time basename (`moleculeSource`'s `name`, extension only, no directory), not a usable\n * write target — the absolute `path` prop already in scope is what gets written.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { MolViewer, type MolViewerHandle, type SaveEvent } from \"@molviewer/core\";\nimport \"@molviewer/core/style.css\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { StatusBadge } from \"@pi-studio-ui/components/primitives/StatusBadge.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { useFileWatch } from \"@pi-studio-ui/hooks/use-file-watch.js\";\nimport { moleculeSource } from \"./molecule-source.js\";\nimport { shouldApplyRefresh } from \"./molecule-reload.js\";\nimport { writeFile } from \"./write-file.js\";\nimport { MOLVIEWER_THEME } from \"./molecule-theme.js\";\nimport styles from \"./MoleculeViewer.module.css\";\n\nexport interface MoleculeViewerProps {\n /** Absolute path of the file to render, or null for an empty (\"New molecule view\") tab. */\n path: string | null;\n /** True when this viewer's tab is the visible one. Molstar's own `ResizeObserver` on its canvas\n * container re-fits on becoming visible (verified in the installed bundle), so this is not\n * currently used to drive a manual re-fit — kept as the escape hatch task-010's visual\n * verification may still need. */\n isActive?: boolean;\n /** Mirrors `MolViewerProps.onModifiedChange` upward for a future parent-level consumer (e.g. a\n * tab-strip \"unsaved changes\" mark) — independent of this component's own internal `modified`\n * state below, which drives the live-reload gate. */\n onModifiedChange?: (modified: boolean) => void;\n}\n\nexport function MoleculeViewer({ path, isActive, onModifiedChange }: MoleculeViewerProps) {\n const client = useConnectionStore((s) => s.client);\n const download = useFileDownload(path ?? \"\", Boolean(path));\n const { changedAt } = useFileWatch(path);\n const handleRef = useRef<MolViewerHandle>(null);\n // First load of this tab's file refits the camera (\"replace\"); every reload after that (the\n // same file changing on disk, task-007) preserves camera/selection/undo (\"update\"). A mounted\n // MoleculeViewer corresponds to exactly one tab for its whole lifetime (task-004 mints a new\n // tab id per path), so this never needs to reset for a \"new\" path mid-lifetime.\n const hasLoadedRef = useRef(false);\n const [loadError, setLoadError] = useState<Error | null>(null);\n const [modified, setModified] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n // Ref, not state: recording \"which push we last acted on\" doesn't need its own render — it's\n // read by the same effect that writes it, and by the stale-indicator check below.\n const lastAppliedAtRef = useRef<number | null>(null);\n\n const source = moleculeSource(path, download.data?.objectUrl);\n\n useEffect(() => {\n if (!shouldApplyRefresh({ changedAt, lastAppliedAt: lastAppliedAtRef.current, modified })) {\n return;\n }\n lastAppliedAtRef.current = changedAt;\n void download.refetch();\n // Only `download.refetch` itself needs to be stable across renders for this effect to behave\n // correctly; depending on the whole `download` query object would re-run on every unrelated\n // status change it produces.\n }, [changedAt, modified, download.refetch]);\n\n // A push arrived but couldn't be applied because of unsaved edits — surface that rather than\n // silently diverging. Reading the ref during render is safe here: it only ever changes inside\n // the effect above, which always runs before the next paint that could observe a stale value.\n const hasUnappliedChange =\n changedAt !== null && changedAt !== lastAppliedAtRef.current && modified;\n\n async function handleSave(e: SaveEvent) {\n if (!path) return;\n if (!client) {\n setSaveError(\"Not connected — cannot save.\");\n return;\n }\n setSaveError(null);\n try {\n await writeFile(client, path, e.text());\n e.saved();\n } catch (err) {\n setSaveError(err instanceof Error ? err.message : \"Failed to save\");\n }\n }\n\n const downloadErrorMessage = download.isError\n ? download.error instanceof Error\n ? download.error.message\n : \"download failed\"\n : null;\n const errorMessage = downloadErrorMessage ?? loadError?.message ?? null;\n\n if (path && download.isPending) {\n return (\n <Panel>\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n </Panel>\n );\n }\n if (errorMessage) {\n return (\n <Panel>\n <EmptyState>Error: {errorMessage}</EmptyState>\n </Panel>\n );\n }\n\n return (\n <Panel className={styles.wrap} data-molecule-active={isActive ? \"true\" : \"false\"}>\n <div className={styles.badges}>\n {hasUnappliedChange && <StatusBadge label=\"File changed on disk\" variant=\"muted\" />}\n {saveError && <StatusBadge label={`Save failed: ${saveError}`} variant=\"error\" />}\n </div>\n <MolViewer\n ref={handleRef}\n className={styles.molViewer}\n theme={MOLVIEWER_THEME}\n source={source}\n sourceMode={hasLoadedRef.current ? \"update\" : \"replace\"}\n onLoad={() => {\n hasLoadedRef.current = true;\n setLoadError(null);\n }}\n onLoadError={(e) => setLoadError(e.error)}\n onModifiedChange={(m) => {\n setModified(m);\n onModifiedChange?.(m);\n }}\n onSave={path ? handleSave : undefined}\n />\n </Panel>\n );\n}\n","/**\n * MoleculeViewerPanel — thin `PanelProps` adapter for a `kind: \"molecule\"` tab. No header, no\n * File/Diff toggle (a molecule tab has neither — diffs come from the git Changes panel\n * independently, `ChangesPanel.tsx`); it owns the panel-filling height chain and hands off to\n * `MoleculeViewer` for everything else.\n */\n\nimport { useIsTabVisible, type MoleculeTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { PanelProps } from \"@pi-studio-ui/features/workspace/panel-registry.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { MoleculeViewer } from \"./MoleculeViewer.js\";\n\nexport function MoleculeViewerPanel({ tab }: PanelProps) {\n const { path } = tab.data as MoleculeTabData;\n // Per-pane visibility: a molecule tab in a non-focused pane is still on screen and must resize.\n const isActive = useIsTabVisible(tab.id);\n\n return (\n <Panel>\n <MoleculeViewer path={path} isActive={isActive} />\n </Panel>\n );\n}\n"],"names":["moleculeSource","path","objectUrl","shouldApplyRefresh","changedAt","lastAppliedAt","modified","ERROR_MESSAGES","writeFile","client","content","response","code","MOLVIEWER_THEME","MoleculeViewer","isActive","onModifiedChange","useConnectionStore","s","download","useFileDownload","useFileWatch","handleRef","useRef","hasLoadedRef","loadError","setLoadError","useState","setModified","saveError","setSaveError","lastAppliedAtRef","source","_a","useEffect","hasUnappliedChange","handleSave","e","err","errorMessage","jsx","Panel","jsxs","EmptyState","Spinner","styles","StatusBadge","MolViewer","m","MoleculeViewerPanel","tab","useIsTabVisible"],"mappings":"kjBASO,SAASA,EACdC,EACAC,EACwB,CACxB,MAAI,CAACD,GAAQ,CAACC,EAAkB,KACzB,CAAE,IAAKA,EAAW,KAAMD,EAAK,MAAM,GAAG,EAAE,IAAA,GAASA,CAAA,CAC1D,CCIO,SAASE,EAAmB,CACjC,UAAAC,EACA,cAAAC,EACA,SAAAC,CACF,EAAoC,CAGlC,MAFI,EAAAF,IAAc,MACdE,GACAF,IAAcC,EAEpB,CClBA,MAAME,EAAyC,CAC7C,WAAY,mBACZ,UAAW,0CACX,UAAW,6BACX,WAAY,8BACZ,WAAY,0BACd,EAIA,eAAsBC,EACpBC,EACAR,EACAS,EACe,CACf,MAAMC,EAAW,MAAMF,EAAO,WAAW,QACvC,qBACA,CAAE,KAAAR,EAAM,QAAAS,CAAA,CAAQ,EAElB,GAAI,CAACC,EAAS,GAAI,CAChB,MAAMC,EAAOD,EAAS,OAAS,GAC/B,MAAM,IAAI,MAAMJ,EAAeK,CAAI,GAAKA,GAAQ,gBAAgB,CAClE,CACF,CCJO,MAAMC,EAA0C,CAErD,WAAY,2BACZ,SAAU,2BACV,WAAY,2BACZ,WAAY,2BAEZ,YAAa,2BACb,kBAAmB,2BACnB,yBAA0B,2BAC1B,mBAAoB,yBACpB,iBAAkB,yBAElB,YAAa,2BACb,uBAAwB,2BACxB,cAAe,2BACf,mBAAoB,yBAEpB,iBAAkB,6BAClB,oBAAqB,4BACvB,+FCQO,SAASC,EAAe,CAAE,KAAAb,EAAM,SAAAc,EAAU,iBAAAC,GAAyC,OACxF,MAAMP,EAASQ,EAAoBC,GAAMA,EAAE,MAAM,EAC3CC,EAAWC,EAAgBnB,GAAQ,GAAI,EAAQA,CAAK,EACpD,CAAE,UAAAG,CAAA,EAAciB,EAAapB,CAAI,EACjCqB,EAAYC,EAAAA,OAAwB,IAAI,EAKxCC,EAAeD,EAAAA,OAAO,EAAK,EAC3B,CAACE,EAAWC,CAAY,EAAIC,EAAAA,SAAuB,IAAI,EACvD,CAACrB,EAAUsB,CAAW,EAAID,EAAAA,SAAS,EAAK,EACxC,CAACE,EAAWC,CAAY,EAAIH,EAAAA,SAAwB,IAAI,EAGxDI,EAAmBR,EAAAA,OAAsB,IAAI,EAE7CS,EAAShC,EAAeC,GAAMgC,EAAAd,EAAS,OAAT,YAAAc,EAAe,SAAS,EAE5DC,EAAAA,UAAU,IAAM,CACT/B,EAAmB,CAAE,UAAAC,EAAW,cAAe2B,EAAiB,QAAS,SAAAzB,CAAA,CAAU,IAGxFyB,EAAiB,QAAU3B,EACtBe,EAAS,QAAA,EAIhB,EAAG,CAACf,EAAWE,EAAUa,EAAS,OAAO,CAAC,EAK1C,MAAMgB,EACJ/B,IAAc,MAAQA,IAAc2B,EAAiB,SAAWzB,EAElE,eAAe8B,EAAWC,EAAc,CACtC,GAAKpC,EACL,IAAI,CAACQ,EAAQ,CACXqB,EAAa,8BAA8B,EAC3C,MACF,CACAA,EAAa,IAAI,EACjB,GAAI,CACF,MAAMtB,EAAUC,EAAQR,EAAMoC,EAAE,MAAM,EACtCA,EAAE,MAAA,CACJ,OAASC,EAAK,CACZR,EAAaQ,aAAe,MAAQA,EAAI,QAAU,gBAAgB,CACpE,EACF,CAOA,MAAMC,GALuBpB,EAAS,QAClCA,EAAS,iBAAiB,MACxBA,EAAS,MAAM,QACf,kBACF,QACyCM,GAAA,YAAAA,EAAW,UAAW,KAEnE,OAAIxB,GAAQkB,EAAS,UAEjBqB,EAAAA,IAACC,EAAA,CACC,SAAAC,EAAAA,KAACC,EAAA,CACC,SAAA,CAAAH,EAAAA,IAACI,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,CAAA,CACvB,CAAA,CACF,EAGAL,EAEAC,EAAAA,IAACC,EAAA,CACC,SAAAC,EAAAA,KAACC,EAAA,CAAW,SAAA,CAAA,UAAQJ,CAAA,CAAA,CAAa,CAAA,CACnC,EAKFG,OAACD,GAAM,UAAWI,EAAO,KAAM,uBAAsB9B,EAAW,OAAS,QACvE,SAAA,CAAA2B,EAAAA,KAAC,MAAA,CAAI,UAAWG,EAAO,OACpB,SAAA,CAAAV,GAAsBK,EAAAA,IAACM,EAAA,CAAY,MAAM,uBAAuB,QAAQ,QAAQ,EAChFjB,SAAciB,EAAA,CAAY,MAAO,gBAAgBjB,CAAS,GAAI,QAAQ,OAAA,CAAQ,CAAA,EACjF,EACAW,EAAAA,IAACO,EAAA,CACC,IAAKzB,EACL,UAAWuB,EAAO,UAClB,MAAOhC,EACP,OAAAmB,EACA,WAAYR,EAAa,QAAU,SAAW,UAC9C,OAAQ,IAAM,CACZA,EAAa,QAAU,GACvBE,EAAa,IAAI,CACnB,EACA,YAAcW,GAAMX,EAAaW,EAAE,KAAK,EACxC,iBAAmBW,GAAM,CACvBpB,EAAYoB,CAAC,EACbhC,GAAA,MAAAA,EAAmBgC,EACrB,EACA,OAAQ/C,EAAOmC,EAAa,MAAA,CAAA,CAC9B,EACF,CAEJ,CCjJO,SAASa,GAAoB,CAAE,IAAAC,GAAmB,CACvD,KAAM,CAAE,KAAAjD,GAASiD,EAAI,KAEfnC,EAAWoC,EAAgBD,EAAI,EAAE,EAEvC,aACGT,EAAA,CACC,SAAAD,EAAAA,IAAC1B,EAAA,CAAe,KAAAb,EAAY,SAAAc,EAAoB,EAClD,CAEJ"}
|