@av-pi-studio/web-client 0.0.69 → 0.0.71

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.
Files changed (31) hide show
  1. package/dist/web/assets/{BinaryFallbackViewer-CY-sOJ-s.js → BinaryFallbackViewer-BWzvrIhq.js} +2 -2
  2. package/dist/web/assets/{BinaryFallbackViewer-CY-sOJ-s.js.map → BinaryFallbackViewer-BWzvrIhq.js.map} +1 -1
  3. package/dist/web/assets/{ChatPanel-CHvnaWVh.js → ChatPanel-CEH_GK7N.js} +2 -2
  4. package/dist/web/assets/{ChatPanel-CHvnaWVh.js.map → ChatPanel-CEH_GK7N.js.map} +1 -1
  5. package/dist/web/assets/{CodeView-BG2-voMV.js → CodeView-vzOAirYe.js} +2 -2
  6. package/dist/web/assets/{CodeView-BG2-voMV.js.map → CodeView-vzOAirYe.js.map} +1 -1
  7. package/dist/web/assets/{FilePanel-D0C36TRP.js → FilePanel-WQVK2120.js} +2 -2
  8. package/dist/web/assets/{FilePanel-D0C36TRP.js.map → FilePanel-WQVK2120.js.map} +1 -1
  9. package/dist/web/assets/{ImageViewer-YJE3nuMb.js → ImageViewer-Bc0i-JGb.js} +2 -2
  10. package/dist/web/assets/{ImageViewer-YJE3nuMb.js.map → ImageViewer-Bc0i-JGb.js.map} +1 -1
  11. package/dist/web/assets/{MarkdownFileViewer-Dio7ZIAS.js → MarkdownFileViewer-I0b0agu_.js} +2 -2
  12. package/dist/web/assets/{MarkdownFileViewer-Dio7ZIAS.js.map → MarkdownFileViewer-I0b0agu_.js.map} +1 -1
  13. package/dist/web/assets/{MoleculeViewerPanel-Cw5RrDrk.js → MoleculeViewerPanel-DYKfUyuF.js} +2 -2
  14. package/dist/web/assets/{MoleculeViewerPanel-Cw5RrDrk.js.map → MoleculeViewerPanel-DYKfUyuF.js.map} +1 -1
  15. package/dist/web/assets/{TerminalPanel-BHj10l7-.js → TerminalPanel-Do6WsYb9.js} +2 -2
  16. package/dist/web/assets/TerminalPanel-Do6WsYb9.js.map +1 -0
  17. package/dist/web/assets/{TextViewer-DdViYQah.js → TextViewer-C7U5fAYp.js} +2 -2
  18. package/dist/web/assets/{TextViewer-DdViYQah.js.map → TextViewer-C7U5fAYp.js.map} +1 -1
  19. package/dist/web/assets/{VideoViewer-rhO-urrJ.js → VideoViewer-DeIrV1Ql.js} +2 -2
  20. package/dist/web/assets/{VideoViewer-rhO-urrJ.js.map → VideoViewer-DeIrV1Ql.js.map} +1 -1
  21. package/dist/web/assets/{index-DM5KSYdn.js → index-DonKPQN3.js} +3 -3
  22. package/dist/web/assets/{index-DM5KSYdn.js.map → index-DonKPQN3.js.map} +1 -1
  23. package/dist/web/assets/{markdown-BLewcN0h.js → markdown-DRKdD_IM.js} +2 -2
  24. package/dist/web/assets/{markdown-BLewcN0h.js.map → markdown-DRKdD_IM.js.map} +1 -1
  25. package/dist/web/assets/{use-file-download-BOCZENOG.js → use-file-download-C2Lrmbfe.js} +2 -2
  26. package/dist/web/assets/{use-file-download-BOCZENOG.js.map → use-file-download-C2Lrmbfe.js.map} +1 -1
  27. package/dist/web/assets/{use-file-watch-CWtZGJOx.js → use-file-watch-skGpUUS7.js} +2 -2
  28. package/dist/web/assets/{use-file-watch-CWtZGJOx.js.map → use-file-watch-skGpUUS7.js.map} +1 -1
  29. package/dist/web/index.html +1 -1
  30. package/package.json +3 -3
  31. package/dist/web/assets/TerminalPanel-BHj10l7-.js.map +0 -1
@@ -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-DM5KSYdn.js";import{u as S,c as k}from"./vendor-query-BGIh-vlp.js";import{u as q}from"./use-file-watch-CWtZGJOx.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-D0C36TRP.js.map
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-DonKPQN3.js";import{u as S,c as k}from"./vendor-query-BGIh-vlp.js";import{u as q}from"./use-file-watch-skGpUUS7.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-WQVK2120.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FilePanel-D0C36TRP.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-WQVK2120.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":"wgBAiBO,SAASA,EAAYC,EAAcC,EAAaC,EAAiBC,EAAU,GAAM,CACtF,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAEjD,OAAOC,EAAS,CACd,SAAUC,EAAQ,SAASR,EAAMC,EAAKC,CAAM,EAC5C,QAAS,SAAqC,CAC5C,GAAI,CAACE,EAAQ,MAAM,IAAI,MAAM,eAAe,EAM5C,MAAO,CAAE,OALQ,MAAMA,EAAO,WAAW,QAA6B,oBAAqB,CACzF,KAAAJ,EACA,IAAAC,EACA,OAAAC,CAAA,CACD,GACwB,OAAS,EAAA,CACpC,EACA,QAAS,EAAQE,GAAW,EAAQJ,GAAS,EAAQC,GAAQE,CAAA,CAC9D,CACH,CCXO,MAAMM,EAA8C,IAAI,IAAI,CAAC,OAAQ,WAAY,OAAO,CAAC,EAgBzF,SAASC,EAAmBV,EAAcC,EAAaU,EAA8B,CAC1F,MAAMC,EAAcC,EAAA,EAGdC,EAASL,EAAmB,IAAIE,CAAU,EAAII,EAAqBf,EAAMC,CAAG,EAAI,KAChF,CAAE,UAAAe,CAAA,EAAcC,EAAaH,CAAM,EAEzCI,EAAAA,UAAU,IAAM,CACVF,IAAc,OACbJ,EAAY,kBAAkB,CAAE,SAAUJ,EAAQ,SAASR,CAAI,EAAG,EAClEY,EAAY,kBAAkB,CAAE,SAAUJ,EAAQ,aAAaR,CAAI,EAAG,EACtEY,EAAY,kBAAkB,CAAE,SAAUJ,EAAQ,eAAeR,CAAI,EAAG,EAC/E,EAAG,CAACgB,EAAWhB,EAAMY,CAAW,CAAC,CACnC,4JCfA,SAASO,EAAQC,EAAsD,CACrE,MAAO,SAAUA,CACnB,CAEA,SAASC,EAAcD,EAAsD,CAC3E,MAAO,WAAYA,CACrB,CAMO,SAASE,EAAU,CAAE,IAAAC,GAAuB,OACjD,MAAMC,EAAYD,EAAI,OAAS,OACzBH,EAAOG,EAAI,KACXvB,EAAOmB,EAAQC,CAAI,EAAIA,EAAK,KAAO,GACnClB,EAASiB,EAAQC,CAAI,GAAKC,EAAcD,CAAI,EAAIA,EAAK,OAAS,GAE9D,CAACK,EAAUC,CAAW,EAAIC,EAAAA,SAAmBH,EAAY,OAAS,MAAM,EACxEvB,EAAMsB,EAAI,cAAgB,IAC1BZ,EAAaiB,EAAiB5B,CAAI,EACxCU,EAAmBV,EAAMC,EAAKU,CAAU,EAExC,MAAMkB,EAAY9B,EAAYC,EAAMC,EAAKC,EAAQuB,IAAa,MAAM,EAE9DK,EAASC,EAAepB,CAAU,EAExC,cACGqB,EAAA,CACC,SAAA,CAAAC,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,OACrB,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAWD,EAAO,KAAO,SAAAlC,EAAK,EACpCmC,EAAAA,IAAC,OAAA,CAAK,UAAWD,EAAO,KACrB,aAAa,OAAUhC,EAAS,SAAW,WAAc,IAAA,CAC5D,EACA+B,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,WACrB,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAK,KACL,QAASX,IAAa,OAAS,UAAY,QAC3C,QAAS,IAAMC,EAAY,MAAM,EAClC,SAAA,MAAA,CAAA,EAGDS,EAAAA,IAACC,EAAA,CACC,KAAK,KACL,QAASX,IAAa,OAAS,UAAY,QAC3C,QAAS,IAAMC,EAAY,MAAM,EAClC,SAAA,MAAA,CAAA,CAED,CAAA,CACF,CAAA,EACF,EACAS,EAAAA,IAAC,OAAI,UAAWE,EAAKH,EAAO,KAAM,WAAW,EAC1C,SAAAT,IAAa,OACZU,EAAAA,IAACG,EAAAA,SAAA,CACC,gBACGC,EAAA,CACC,SAAA,CAAAJ,EAAAA,IAACK,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,EACvB,EAGF,SAAAL,EAAAA,IAACL,GAAO,KAAA9B,CAAA,CAAY,CAAA,CAAA,EAEpB6B,EAAU,UACZI,EAAAA,KAACM,EAAA,CACC,SAAA,CAAAJ,EAAAA,IAACK,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,kBAAA,CAAA,CACvB,EACEX,EAAU,QACZI,EAAAA,KAACM,EAAA,CAAW,SAAA,CAAA,UACFV,EAAU,iBAAiB,MAAQA,EAAU,MAAM,QAAU,eAAA,EACvE,GACEY,EAAAZ,EAAU,OAAV,MAAAY,EAAgB,MAClBN,MAACO,EAAA,CAAS,MAAOb,EAAU,KAAK,MAAO,EAEvCM,EAAAA,IAACI,GAAW,MAAO,CAAE,QAAS,wBAA0B,0CAExD,CAAA,CAEJ,CAAA,EACF,CAEJ"}
@@ -1,2 +1,2 @@
1
- import{j as o}from"./vendor-react-CWo7Do4s.js";import{E as e,S as t}from"./index-DM5KSYdn.js";import{u as a}from"./use-file-download-BOCZENOG.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-YJE3nuMb.js.map
1
+ import{j as o}from"./vendor-react-CWo7Do4s.js";import{E as e,S as t}from"./index-DonKPQN3.js";import{u as a}from"./use-file-download-C2Lrmbfe.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-Bc0i-JGb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImageViewer-YJE3nuMb.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
+ {"version":3,"file":"ImageViewer-Bc0i-JGb.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-DM5KSYdn.js";import{u as l,C as m}from"./CodeView-BG2-voMV.js";import{M as p}from"./markdown-BLewcN0h.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-Dio7ZIAS.js.map
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-DonKPQN3.js";import{u as l,C as m}from"./CodeView-vzOAirYe.js";import{M as p}from"./markdown-DRKdD_IM.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-I0b0agu_.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownFileViewer-Dio7ZIAS.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
+ {"version":3,"file":"MarkdownFileViewer-I0b0agu_.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 A,P as c,E as S,S as F,h as L}from"./index-DM5KSYdn.js";import{m as N}from"./vendor-molviewer-DE5Dt5UM.js";import{S as b}from"./StatusBadge-DFSeaZ3p.js";import{u as T}from"./use-file-download-BOCZENOG.js";import{u as P}from"./use-file-watch-CWtZGJOx.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 B(e,o){return!e||!o?null:{url:o,name:e.split("/").pop()||e}}function k({changedAt:e,lastAppliedAt:o,modified:t}){return!(e===null||t||e===o)}const q={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 I(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(q[s]??s??"Failed to save")}}const O={"--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)"},U="_wrap_tixu9_1",W="_molViewer_tixu9_4",z="_badges_tixu9_8",m={wrap:U,molViewer:W,badges:z};function D({path:e,isActive:o,onModifiedChange:t}){var g;const l=A(a=>a.client),s=T(e??"",!!e),{changedAt:i}=P(e),j=n.useRef(null),v=n.useRef(!1),[u,w]=n.useState(null),[d,R]=n.useState(!1),[x,f]=n.useState(null),p=n.useRef(null),E=((g=s.data)==null?void 0:g.objectUrl)??null,M=n.useMemo(()=>B(e,E),[e,E]);n.useEffect(()=>{k({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 V(a){if(e){if(!l){f("Not connected — cannot save.");return}f(null);try{await I(l,e,a.text()),a.saved()}catch(_){f(_ instanceof Error?_.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(S,{children:[r.jsx(F,{size:"sm"})," Loading..."]})}):h?r.jsx(c,{children:r.jsxs(S,{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(b,{label:"File changed on disk",variant:"muted"}),x&&r.jsx(b,{label:`Save failed: ${x}`,variant:"error"})]}),r.jsx(N,{ref:j,className:m.molViewer,theme:O,source:M,sourceMode:v.current?"update":"replace",onLoad:()=>{v.current=!0,w(null)},onLoadError:a=>w(a.error),onModifiedChange:a=>{R(a),t==null||t(a)},onSave:e?V:void 0})]})}function te({tab:e}){const{path:o}=e.data,t=L(e.id);return r.jsx(c,{children:r.jsx(D,{path:o,isActive:t})})}export{te as MoleculeViewerPanel};
2
- //# sourceMappingURL=MoleculeViewerPanel-Cw5RrDrk.js.map
1
+ import{r as n,j as r}from"./vendor-react-CWo7Do4s.js";import{a as A,P as c,E as S,S as F,h as L}from"./index-DonKPQN3.js";import{m as N}from"./vendor-molviewer-DE5Dt5UM.js";import{S as b}from"./StatusBadge-DFSeaZ3p.js";import{u as T}from"./use-file-download-C2Lrmbfe.js";import{u as P}from"./use-file-watch-skGpUUS7.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 B(e,o){return!e||!o?null:{url:o,name:e.split("/").pop()||e}}function k({changedAt:e,lastAppliedAt:o,modified:t}){return!(e===null||t||e===o)}const q={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 I(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(q[s]??s??"Failed to save")}}const O={"--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)"},U="_wrap_tixu9_1",W="_molViewer_tixu9_4",z="_badges_tixu9_8",m={wrap:U,molViewer:W,badges:z};function D({path:e,isActive:o,onModifiedChange:t}){var g;const l=A(a=>a.client),s=T(e??"",!!e),{changedAt:i}=P(e),j=n.useRef(null),v=n.useRef(!1),[u,w]=n.useState(null),[d,R]=n.useState(!1),[x,f]=n.useState(null),p=n.useRef(null),E=((g=s.data)==null?void 0:g.objectUrl)??null,M=n.useMemo(()=>B(e,E),[e,E]);n.useEffect(()=>{k({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 V(a){if(e){if(!l){f("Not connected — cannot save.");return}f(null);try{await I(l,e,a.text()),a.saved()}catch(_){f(_ instanceof Error?_.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(S,{children:[r.jsx(F,{size:"sm"})," Loading..."]})}):h?r.jsx(c,{children:r.jsxs(S,{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(b,{label:"File changed on disk",variant:"muted"}),x&&r.jsx(b,{label:`Save failed: ${x}`,variant:"error"})]}),r.jsx(N,{ref:j,className:m.molViewer,theme:O,source:M,sourceMode:v.current?"update":"replace",onLoad:()=>{v.current=!0,w(null)},onLoadError:a=>w(a.error),onModifiedChange:a=>{R(a),t==null||t(a)},onSave:e?V:void 0})]})}function te({tab:e}){const{path:o}=e.data,t=L(e.id);return r.jsx(c,{children:r.jsx(D,{path:o,isActive:t})})}export{te as MoleculeViewerPanel};
2
+ //# sourceMappingURL=MoleculeViewerPanel-DYKfUyuF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MoleculeViewerPanel-Cw5RrDrk.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, useMemo, 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 // Memoized, and that is load-bearing. `MolViewer`'s `source` prop is a load TRIGGER keyed on\n // object IDENTITY (`useEffect(…, [source])` in @molviewer/core, documented there as \"loaded\n // whenever this value's identity changes\"), so a fresh `{ url, name }` literal per render is a\n // reload command: `UPDATE_SYSTEM` re-parses the file and silently reverts every in-viewer edit.\n // Symptom when this is missing: the FIRST atom delete/move after a load or a save undoes itself\n // — the edit flips `modified`, `onModifiedChange` → `setModified` re-renders, and the new\n // identity reloads the molecule out from under the edit. Later edits stuck (no `modified`\n // transition, so no re-render), which is what made it look intermittent. Every unrelated\n // re-render did it too, and `TabPanelHost` re-renders all panels on every layout mutation —\n // a divider drag reloaded the structure once per pointermove frame.\n // Identity must change only when the bytes do: when `refetch()` mints a new object URL.\n const objectUrl = download.data?.objectUrl ?? null;\n const source = useMemo(() => moleculeSource(path, objectUrl), [path, 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","_a","source","useMemo","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-DYKfUyuF.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, useMemo, 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 // Memoized, and that is load-bearing. `MolViewer`'s `source` prop is a load TRIGGER keyed on\n // object IDENTITY (`useEffect(…, [source])` in @molviewer/core, documented there as \"loaded\n // whenever this value's identity changes\"), so a fresh `{ url, name }` literal per render is a\n // reload command: `UPDATE_SYSTEM` re-parses the file and silently reverts every in-viewer edit.\n // Symptom when this is missing: the FIRST atom delete/move after a load or a save undoes itself\n // — the edit flips `modified`, `onModifiedChange` → `setModified` re-renders, and the new\n // identity reloads the molecule out from under the edit. Later edits stuck (no `modified`\n // transition, so no re-render), which is what made it look intermittent. Every unrelated\n // re-render did it too, and `TabPanelHost` re-renders all panels on every layout mutation —\n // a divider drag reloaded the structure once per pointermove frame.\n // Identity must change only when the bytes do: when `refetch()` mints a new object URL.\n const objectUrl = download.data?.objectUrl ?? null;\n const source = useMemo(() => moleculeSource(path, objectUrl), [path, 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","_a","source","useMemo","useEffect","hasUnappliedChange","handleSave","e","err","errorMessage","jsx","Panel","jsxs","EmptyState","Spinner","styles","StatusBadge","MolViewer","m","MoleculeViewerPanel","tab","useIsTabVisible"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- var X=Object.defineProperty;var J=(r,e,t)=>e in r?X(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var D=(r,e,t)=>J(r,typeof e!="symbol"?e+"":e,t);import{r as s,j as T}from"./vendor-react-CWo7Do4s.js";import{x as K,a as Q}from"./vendor-terminal-BhzX2_ts.js";import{g as L,a as Z,h as $,i as ee,j as Y,S as te,k as re,l as ne}from"./index-DM5KSYdn.js";import"./vendor-04vXFIXe.js";import"./vendor-query-BGIh-vlp.js";import"./vendor-icons-CJ_dOXcR.js";import"./vendor-overlays-BnmaUbKw.js";import"./vendor-dnd-Bj2_GhfD.js";import"./vendor-highlight-Efc6A-g6.js";import"./vendor-markdown-YvkQLm_s.js";class se{constructor(e){D(this,"subscribers",new Map);D(this,"detach",null);this.daemon=e}start(){this.detach||(this.detach=this.daemon.onTerminalFrame(e=>this.dispatch(e)))}stop(){var e;(e=this.detach)==null||e.call(this),this.detach=null}subscribeSlot(e,t){return this.subscribers.set(e,t),()=>{this.subscribers.get(e)===t&&this.subscribers.delete(e)}}hasSlot(e){return this.subscribers.has(e)}sendInput(e,t){this.daemon.sendBinary(L({opcode:"Input",slot:e,data:t}))}sendResize(e,t,d){this.daemon.sendBinary(L({opcode:"Resize",slot:e,rows:t,cols:d}))}dispatch(e){var d,y,p;const t=this.subscribers.get(e.slot);if(t)switch(e.opcode){case"Output":(d=t.onOutput)==null||d.call(t,e.data);return;case"Snapshot":(y=t.onSnapshot)==null||y.call(t,e.data);return;case"Restore":(p=t.onRestore)==null||p.call(t,e.data);return;default:return}}}const oe=2,ce=1;function _(r){if(r==null)return!1;const{cols:e,rows:t}=r;return typeof e=="number"&&Number.isInteger(e)&&e>=oe&&typeof t=="number"&&Number.isInteger(t)&&t>=ce}function ue(r,e){return r===null||e===null?r===e:r.cols===e.cols&&r.rows===e.rows}function ie(r,e){return _(r)?!ue(r,e):!1}const ae="_wrap_7fmht_1",le="_terminal_7fmht_12",fe="_statusOverlay_7fmht_19",M={wrap:ae,terminal:le,statusOverlay:fe},de=64*1024,G=new TextEncoder,W=new WeakMap;function me(r){let e=W.get(r);return e||(e=new se(r),e.start(),W.set(r,e)),e}function V(r){const e=Y.getState(),t=e.tabs.find(d=>d.id===r);return!t||t.workspaceCwd!==e.activeWorkspaceCwd?!1:re(ne.getState().layouts[t.workspaceCwd],r)}const pe={background:"#181b1a",foreground:"#fafafa",cursor:"#a2b4d7",cursorAccent:"#181b1a",selectionBackground:"rgba(255,255,255,0.18)",black:"#18181b",red:"#ef4444",green:"#22c55e",yellow:"#f59e0b",blue:"#3b82f6",magenta:"#a855f7",cyan:"#14b8a6",white:"#d4d4d8",brightBlack:"#52525b",brightRed:"#f87171",brightGreen:"#4ade80",brightYellow:"#fbbf24",brightBlue:"#60a5fa",brightMagenta:"#c084fc",brightCyan:"#2dd4bf",brightWhite:"#fafafa"};function Te({tab:r}){const e=r.data,t=Z(n=>n.client),d=$(r.id),y=s.useRef(null),p=s.useRef(null),R=s.useRef(null),v=s.useRef(e.slot),q=s.useRef(null),I=s.useRef([]),k=s.useRef(0),z=s.useRef(null),A=s.useRef(null),w=s.useRef(null),E=s.useRef(null),N=s.useRef(!1),j=s.useRef(t);j.current=t;const[g,H]=s.useState(e.slot),[O,F]=s.useState({isAttaching:!0,error:null}),x=()=>{var c,a;const n=(c=R.current)==null?void 0:c.proposeDimensions();(a=A.current)==null||a.call(A,_(n)?n:null)},U=()=>{var n;E.current=null,N.current=!0,(n=R.current)==null||n.fit(),x(),requestAnimationFrame(()=>{N.current=!1})},P=()=>{w.current!==null&&window.clearTimeout(w.current),w.current=window.setTimeout(()=>{w.current=null,E.current===null&&(E.current=requestAnimationFrame(U))},60)};s.useEffect(()=>{const n=y.current;if(!n)return;const c=new K.Terminal({cursorBlink:!0,fontFamily:"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",fontSize:ee.sm,scrollback:5e3,theme:pe,allowProposedApi:!0}),a=new Q.FitAddon;c.loadAddon(a),c.open(n);const m=o=>{if(!V(r.id))return;const i=v.current,b=q.current;i===null||!b||ie(o,z.current)&&(z.current=o,b.sendResize(i,o.rows,o.cols))},l=c.onData(o=>{const i=v.current,b=q.current;if(i===null||!b){const C=G.encode(o);k.current+C.length<=de&&(I.current.push(C),k.current+=C.length);return}b.sendInput(i,G.encode(o))}),f=c.onResize(({cols:o,rows:i})=>{m({cols:o,rows:i})}),h=()=>{a.fit(),x()};n.addEventListener("focusin",h),a.fit(),p.current=c,R.current=a,A.current=m;const u=new ResizeObserver(o=>{if(N.current)return;const i=o[0];i&&(i.contentRect.width===0||i.contentRect.height===0)||P()});return u.observe(n),()=>{u.disconnect(),w.current!==null&&window.clearTimeout(w.current),E.current!==null&&cancelAnimationFrame(E.current),n.removeEventListener("focusin",h),l.dispose(),f.dispose(),c.dispose(),p.current=null,R.current=null,A.current=null}},[]);const S=s.useRef(!1),B=s.useRef(!1);return s.useEffect(()=>{var m;if(S.current=!0,!t||v.current!==null||B.current)return()=>{S.current=!1};B.current=!0;const n=e.cwd||"~",c=(m=R.current)==null?void 0:m.proposeDimensions(),a=_(c)?c:null;return t.connection.request("create_terminal_request",{workspaceId:"",cwd:n,...a?{cols:a.cols,rows:a.rows}:{}}).then(l=>{const f=l.terminal.slot;if(!S.current){t.connection.request("kill_terminal_request",{slot:f}).catch(()=>{});return}v.current=f,H(f),Y.getState().updateData(r.id,{slot:f});const h={cols:l.terminal.cols,rows:l.terminal.rows};z.current=_(h)?h:null}).catch(l=>{S.current&&F({isAttaching:!1,error:l instanceof Error?l.message:String(l)})}).finally(()=>{B.current=!1}),()=>{S.current=!1}},[t,r.id]),s.useEffect(()=>()=>{const n=v.current;n!==null&&setTimeout(()=>{var c;S.current||(c=j.current)==null||c.connection.request("kill_terminal_request",{slot:n}).catch(()=>{})},0)},[]),s.useEffect(()=>{var h;if(!t||g===null)return;F({isAttaching:!0,error:null});const n=me(t.connection);q.current=n;const c=u=>{var o,i;(o=p.current)==null||o.reset(),(i=p.current)==null||i.write(u)},a=n.subscribeSlot(g,{onOutput:u=>{var o;return(o=p.current)==null?void 0:o.write(u)},onSnapshot:c,onRestore:c});let m=!1;const l=(h=R.current)==null?void 0:h.proposeDimensions(),f=_(l)&&V(r.id)?l:null;return t.connection.request("subscribe_terminal_request",{slot:g,...f?{cols:f.cols,rows:f.rows}:{}}).then(u=>{if(m)return;F({isAttaching:!1,error:null});const o=I.current;I.current=[],k.current=0;for(const b of o)n.sendInput(g,b);const i={cols:u==null?void 0:u.cols,rows:u==null?void 0:u.rows};z.current=_(i)?i:f,x()}).catch(u=>{m||(I.current=[],k.current=0,F({isAttaching:!1,error:u instanceof Error?u.message:String(u)}))}),()=>{m=!0,a(),q.current=null,t.connection.request("unsubscribe_terminal_request",{slot:g}).catch(()=>{})}},[t,g]),s.useEffect(()=>{d&&P()},[d]),T.jsxs("div",{className:M.wrap,children:[T.jsx("div",{ref:y,className:M.terminal}),O.error?T.jsxs("div",{className:M.statusOverlay,children:["Terminal error: ",O.error]}):O.isAttaching?T.jsxs("div",{className:M.statusOverlay,children:[T.jsx(te,{size:"sm"})," Starting terminal…"]}):null]})}export{Te as TerminalPanel};
2
- //# sourceMappingURL=TerminalPanel-BHj10l7-.js.map
1
+ var X=Object.defineProperty;var J=(r,e,t)=>e in r?X(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var D=(r,e,t)=>J(r,typeof e!="symbol"?e+"":e,t);import{r as s,j as T}from"./vendor-react-CWo7Do4s.js";import{x as K,a as Q}from"./vendor-terminal-BhzX2_ts.js";import{g as L,a as Z,h as $,i as ee,j as Y,S as te,k as re,l as ne}from"./index-DonKPQN3.js";import"./vendor-04vXFIXe.js";import"./vendor-query-BGIh-vlp.js";import"./vendor-icons-CJ_dOXcR.js";import"./vendor-overlays-BnmaUbKw.js";import"./vendor-dnd-Bj2_GhfD.js";import"./vendor-highlight-Efc6A-g6.js";import"./vendor-markdown-YvkQLm_s.js";class se{constructor(e){D(this,"subscribers",new Map);D(this,"detach",null);this.daemon=e}start(){this.detach||(this.detach=this.daemon.onTerminalFrame(e=>this.dispatch(e)))}stop(){var e;(e=this.detach)==null||e.call(this),this.detach=null}subscribeSlot(e,t){return this.subscribers.set(e,t),()=>{this.subscribers.get(e)===t&&this.subscribers.delete(e)}}hasSlot(e){return this.subscribers.has(e)}sendInput(e,t){this.daemon.sendBinary(L({opcode:"Input",slot:e,data:t}))}sendResize(e,t,d){this.daemon.sendBinary(L({opcode:"Resize",slot:e,rows:t,cols:d}))}dispatch(e){var d,y,p;const t=this.subscribers.get(e.slot);if(t)switch(e.opcode){case"Output":(d=t.onOutput)==null||d.call(t,e.data);return;case"Snapshot":(y=t.onSnapshot)==null||y.call(t,e.data);return;case"Restore":(p=t.onRestore)==null||p.call(t,e.data);return;default:return}}}const oe=2,ce=1;function _(r){if(r==null)return!1;const{cols:e,rows:t}=r;return typeof e=="number"&&Number.isInteger(e)&&e>=oe&&typeof t=="number"&&Number.isInteger(t)&&t>=ce}function ue(r,e){return r===null||e===null?r===e:r.cols===e.cols&&r.rows===e.rows}function ie(r,e){return _(r)?!ue(r,e):!1}const ae="_wrap_7fmht_1",le="_terminal_7fmht_12",fe="_statusOverlay_7fmht_19",M={wrap:ae,terminal:le,statusOverlay:fe},de=64*1024,G=new TextEncoder,W=new WeakMap;function me(r){let e=W.get(r);return e||(e=new se(r),e.start(),W.set(r,e)),e}function V(r){const e=Y.getState(),t=e.tabs.find(d=>d.id===r);return!t||t.workspaceCwd!==e.activeWorkspaceCwd?!1:re(ne.getState().layouts[t.workspaceCwd],r)}const pe={background:"#181b1a",foreground:"#fafafa",cursor:"#a2b4d7",cursorAccent:"#181b1a",selectionBackground:"rgba(255,255,255,0.18)",black:"#18181b",red:"#ef4444",green:"#22c55e",yellow:"#f59e0b",blue:"#3b82f6",magenta:"#a855f7",cyan:"#14b8a6",white:"#d4d4d8",brightBlack:"#52525b",brightRed:"#f87171",brightGreen:"#4ade80",brightYellow:"#fbbf24",brightBlue:"#60a5fa",brightMagenta:"#c084fc",brightCyan:"#2dd4bf",brightWhite:"#fafafa"};function Te({tab:r}){const e=r.data,t=Z(n=>n.client),d=$(r.id),y=s.useRef(null),p=s.useRef(null),R=s.useRef(null),v=s.useRef(e.slot),q=s.useRef(null),I=s.useRef([]),k=s.useRef(0),z=s.useRef(null),A=s.useRef(null),w=s.useRef(null),E=s.useRef(null),N=s.useRef(!1),j=s.useRef(t);j.current=t;const[g,H]=s.useState(e.slot),[O,F]=s.useState({isAttaching:!0,error:null}),x=()=>{var c,a;const n=(c=R.current)==null?void 0:c.proposeDimensions();(a=A.current)==null||a.call(A,_(n)?n:null)},U=()=>{var n;E.current=null,N.current=!0,(n=R.current)==null||n.fit(),x(),requestAnimationFrame(()=>{N.current=!1})},P=()=>{w.current!==null&&window.clearTimeout(w.current),w.current=window.setTimeout(()=>{w.current=null,E.current===null&&(E.current=requestAnimationFrame(U))},60)};s.useEffect(()=>{const n=y.current;if(!n)return;const c=new K.Terminal({cursorBlink:!0,fontFamily:"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",fontSize:ee.sm,scrollback:5e3,theme:pe,allowProposedApi:!0}),a=new Q.FitAddon;c.loadAddon(a),c.open(n);const m=o=>{if(!V(r.id))return;const i=v.current,b=q.current;i===null||!b||ie(o,z.current)&&(z.current=o,b.sendResize(i,o.rows,o.cols))},l=c.onData(o=>{const i=v.current,b=q.current;if(i===null||!b){const C=G.encode(o);k.current+C.length<=de&&(I.current.push(C),k.current+=C.length);return}b.sendInput(i,G.encode(o))}),f=c.onResize(({cols:o,rows:i})=>{m({cols:o,rows:i})}),h=()=>{a.fit(),x()};n.addEventListener("focusin",h),a.fit(),p.current=c,R.current=a,A.current=m;const u=new ResizeObserver(o=>{if(N.current)return;const i=o[0];i&&(i.contentRect.width===0||i.contentRect.height===0)||P()});return u.observe(n),()=>{u.disconnect(),w.current!==null&&window.clearTimeout(w.current),E.current!==null&&cancelAnimationFrame(E.current),n.removeEventListener("focusin",h),l.dispose(),f.dispose(),c.dispose(),p.current=null,R.current=null,A.current=null}},[]);const S=s.useRef(!1),B=s.useRef(!1);return s.useEffect(()=>{var m;if(S.current=!0,!t||v.current!==null||B.current)return()=>{S.current=!1};B.current=!0;const n=e.cwd||"~",c=(m=R.current)==null?void 0:m.proposeDimensions(),a=_(c)?c:null;return t.connection.request("create_terminal_request",{workspaceId:"",cwd:n,...a?{cols:a.cols,rows:a.rows}:{}}).then(l=>{const f=l.terminal.slot;if(!S.current){t.connection.request("kill_terminal_request",{slot:f}).catch(()=>{});return}v.current=f,H(f),Y.getState().updateData(r.id,{slot:f});const h={cols:l.terminal.cols,rows:l.terminal.rows};z.current=_(h)?h:null}).catch(l=>{S.current&&F({isAttaching:!1,error:l instanceof Error?l.message:String(l)})}).finally(()=>{B.current=!1}),()=>{S.current=!1}},[t,r.id]),s.useEffect(()=>()=>{const n=v.current;n!==null&&setTimeout(()=>{var c;S.current||(c=j.current)==null||c.connection.request("kill_terminal_request",{slot:n}).catch(()=>{})},0)},[]),s.useEffect(()=>{var h;if(!t||g===null)return;F({isAttaching:!0,error:null});const n=me(t.connection);q.current=n;const c=u=>{var o,i;(o=p.current)==null||o.reset(),(i=p.current)==null||i.write(u)},a=n.subscribeSlot(g,{onOutput:u=>{var o;return(o=p.current)==null?void 0:o.write(u)},onSnapshot:c,onRestore:c});let m=!1;const l=(h=R.current)==null?void 0:h.proposeDimensions(),f=_(l)&&V(r.id)?l:null;return t.connection.request("subscribe_terminal_request",{slot:g,...f?{cols:f.cols,rows:f.rows}:{}}).then(u=>{if(m)return;F({isAttaching:!1,error:null});const o=I.current;I.current=[],k.current=0;for(const b of o)n.sendInput(g,b);const i={cols:u==null?void 0:u.cols,rows:u==null?void 0:u.rows};z.current=_(i)?i:f,x()}).catch(u=>{m||(I.current=[],k.current=0,F({isAttaching:!1,error:u instanceof Error?u.message:String(u)}))}),()=>{m=!0,a(),q.current=null,t.connection.request("unsubscribe_terminal_request",{slot:g}).catch(()=>{})}},[t,g]),s.useEffect(()=>{d&&P()},[d]),T.jsxs("div",{className:M.wrap,children:[T.jsx("div",{ref:y,className:M.terminal}),O.error?T.jsxs("div",{className:M.statusOverlay,children:["Terminal error: ",O.error]}):O.isAttaching?T.jsxs("div",{className:M.statusOverlay,children:[T.jsx(te,{size:"sm"})," Starting terminal…"]}):null]})}export{Te as TerminalPanel};
2
+ //# sourceMappingURL=TerminalPanel-Do6WsYb9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TerminalPanel-Do6WsYb9.js","sources":["../../../../client/src/terminal-stream-router.ts","../../../src/features/terminal/terminal-size.ts","../../../src/features/terminal/TerminalPanel.tsx"],"sourcesContent":["import { encodeTerminalFrame, type TerminalFrame } from \"@av-pi-studio/protocol\";\n\nimport type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Client-side demux of binary terminal frames to per-slot subscribers, plus outbound input/resize\n * encoding (architecture/client-app-runtime.md § Router; features/terminals.md § Binary stream\n * protocol).\n *\n * Inbound `Output`/`Snapshot`/`Restore` frames are dispatched to the subscriber registered for that\n * `slot`. Outbound `Input`/`Resize` are encoded with the right opcode + slot and sent on the data\n * path.\n */\n\nexport interface TerminalSlotSubscriber {\n /** Live terminal output bytes. */\n onOutput?: (data: Uint8Array) => void;\n /** Full-screen snapshot bytes (sent on (re)subscribe). */\n onSnapshot?: (data: Uint8Array) => void;\n /** Restore snapshot bytes (reflowable/mode-gated). */\n onRestore?: (data: Uint8Array) => void;\n}\n\nexport class TerminalStreamRouter {\n private readonly subscribers = new Map<number, TerminalSlotSubscriber>();\n private detach: (() => void) | null = null;\n\n constructor(private readonly daemon: DaemonClient) {}\n\n /** Begin routing inbound terminal frames. Idempotent. */\n start(): void {\n if (this.detach) return;\n this.detach = this.daemon.onTerminalFrame((frame) => this.dispatch(frame));\n }\n\n /** Stop routing inbound frames (subscribers retained). */\n stop(): void {\n this.detach?.();\n this.detach = null;\n }\n\n /** Register (or replace) the subscriber for a slot. Returns an unsubscribe fn. */\n subscribeSlot(slot: number, subscriber: TerminalSlotSubscriber): () => void {\n this.subscribers.set(slot, subscriber);\n return () => {\n if (this.subscribers.get(slot) === subscriber) this.subscribers.delete(slot);\n };\n }\n\n /** True iff a subscriber is registered for the slot. */\n hasSlot(slot: number): boolean {\n return this.subscribers.has(slot);\n }\n\n // ─── Outbound ─────────────────────────────────────────────────────────────\n\n /** Send raw input bytes to a slot's PTY (opcode `Input = 0x02`). */\n sendInput(slot: number, data: Uint8Array): void {\n this.daemon.sendBinary(encodeTerminalFrame({ opcode: \"Input\", slot, data }));\n }\n\n /** Send a resize (opcode `Resize = 0x03`, JSON `{ rows, cols }` payload). */\n sendResize(slot: number, rows: number, cols: number): void {\n this.daemon.sendBinary(encodeTerminalFrame({ opcode: \"Resize\", slot, rows, cols }));\n }\n\n // ─── Inbound dispatch ───────────────────────────────────────────────────────\n\n private dispatch(frame: TerminalFrame): void {\n const subscriber = this.subscribers.get(frame.slot);\n if (!subscriber) return; // no subscriber for this slot — drop\n switch (frame.opcode) {\n case \"Output\":\n subscriber.onOutput?.(frame.data);\n return;\n case \"Snapshot\":\n subscriber.onSnapshot?.(frame.data);\n return;\n case \"Restore\":\n subscriber.onRestore?.(frame.data);\n return;\n default:\n // Input/Resize are outbound-only; ignore if echoed back.\n return;\n }\n }\n}\n","/**\n * Terminal PTY size-claim decisions (`swe/features/terminals.md` § PTY size\n * ownership). Pure and DOM-free so the ownership gate is unit-testable under the repo's Node-only\n * vitest environment — `TerminalPanel.tsx` is the only caller.\n *\n * The model separates two things an earlier revision conflated into one `lastClaimed` ref, which is\n * what made restored terminals unfixable-by-resize:\n *\n * - **Knowledge** — `believed`: the grid this client thinks the PTY currently has (from a\n * create-time echo, or from the last size it successfully sent). Only ever used to dedupe.\n * - **Permission** — `isSizeAuthority` in `TerminalPanel.tsx`: whether this panel is the one\n * rendering the terminal in the foreground right now.\n *\n * Conflating them meant \"I have never sent a size\" (`believed === null`, always true for a\n * *restored* terminal, whose PTY predates this client) was read as \"I am not allowed to send one\",\n * so a restored terminal ignored every divider drag and window resize forever.\n */\n\nexport interface Grid {\n cols: number;\n rows: number;\n}\n\n// Mirrors `@xterm/addon-fit`'s own `MINIMUM_COLS`/`MINIMUM_ROWS` (`FitAddon.ts:22-23`) — a grid\n// below this is not a real proposal, it is what `proposeDimensions()` returns while the panel is\n// still settling.\nconst MIN_COLS = 2;\nconst MIN_ROWS = 1;\n\n/** A proposal is usable only if both dimensions are finite integers ≥ the emulator minimum. */\nexport function isMeasurable(proposed: Partial<Grid> | undefined | null): proposed is Grid {\n if (proposed == null) return false;\n const { cols, rows } = proposed;\n return (\n typeof cols === \"number\" &&\n Number.isInteger(cols) &&\n cols >= MIN_COLS &&\n typeof rows === \"number\" &&\n Number.isInteger(rows) &&\n rows >= MIN_ROWS\n );\n}\n\nexport function sameGrid(a: Grid | null, b: Grid | null): boolean {\n if (a === null || b === null) return a === b;\n return a.cols === b.cols && a.rows === b.rows;\n}\n\n/**\n * Whether a measured grid is worth sending as a Resize frame, given what this client believes the\n * PTY's grid already is. Pure dedupe + validity: it answers \"would this frame change anything?\",\n * never \"am I allowed to send it?\" — permission is the caller's `isSizeAuthority` gate.\n *\n * `believed === null` (a restored terminal, whose PTY this client never sized) counts as differing:\n * an unknown remote size is exactly the case that most needs reconciling, since the PTY is\n * typically still at the 80×24 spawn default while the panel renders far wider.\n */\nexport function shouldClaimSize(next: Grid | null, believed: Grid | null): next is Grid {\n if (!isMeasurable(next)) return false;\n return !sameGrid(next, believed);\n}\n","/**\n * TerminalPanel — @xterm/xterm mount + binary-frame streaming via `TerminalStreamRouter`\n * (POC `initTerminalPanel`, POC_TO_APP_PLAN_UI.md §4.6). Strict upgrade over the POC's 800ms\n * `capture_terminal_request` poll: the daemon pushes `Output`/`Snapshot`/`Restore` binary frames\n * directly over the one shared `DaemonClient` connection, demuxed by slot.\n *\n * Slot lifecycle: created once via `create_terminal_request`, then persisted onto the tab's\n * `TerminalTabData.slot` via `useTabStore.getState().updateData` so switching away and back to\n * this tab (kept mounted-but-hidden by `TabPanelHost`) never recreates the terminal. `TabPanelHost`\n * only unmounts a tab's panel when the tab leaves the store's `tabs[]` (i.e. real tab close, never\n * a tab switch) — this component's true-unmount effect below relies on exactly that invariant to\n * send `kill_terminal_request`, terminating the PTY server-side instead of leaking it forever.\n *\n * Mount vs. subscribe are two separate effects (sprint-052/task-001): the emulator (xterm + fit +\n * `onData`/`onResize`) mounts as soon as the container exists, independent of the slot, so\n * `onResize` is attached before the first `fitAddon.fit()` ever runs — closing the window where\n * xterm's one size-changing fit of the panel's life used to fire with no listener. The stream\n * subscription is keyed on `[client, slot]` instead, so a reconnect (new `client`) re-subscribes\n * without tearing down and rebuilding the emulator, preserving scrollback across it.\n *\n * PTY sizing (`terminals.md` § PTY size ownership) is a single seam, `claimSize`, behind two\n * independent gates: `isSizeAuthority` (permission — is this panel the visible renderer, in the\n * active workspace, as its pane's active tab?) and `shouldClaimSize` (validity + dedupe against\n * `believedSizeRef`, what we think the PTY currently is). Keeping knowledge and permission separate\n * is load-bearing: a *restored* terminal's PTY predates this client, so it believes nothing, and an\n * earlier revision that treated \"never sent a size\" as \"not allowed to send one\" left every\n * restored terminal ignoring resizes for its entire life. Every claim funnels through `onResize`\n * (real grid changes) or a `performRefit` reconcile (covers a panel that measured 0×0 while hidden\n * and would otherwise fit to an unchanged grid and stay silent).\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Terminal } from \"@xterm/xterm\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport \"@xterm/xterm/css/xterm.css\";\nimport type { DaemonClient } from \"@av-pi-studio/client\";\nimport { TerminalStreamRouter } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useIsTabVisible, useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { Tab, TerminalTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { isPaneActiveTab, useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { baseFontSize } from \"@pi-studio-ui/theme/tokens.js\";\nimport { isMeasurable, shouldClaimSize, type Grid } from \"./terminal-size.js\";\nimport styles from \"./TerminalPanel.module.css\";\n\nexport interface TerminalPanelProps {\n tab: Tab;\n}\n\n/** `cols`/`rows` echo the PTY's real size. Optional for the same reason as the subscribe echo\n * below: an older daemon may omit them, and a `{cols: undefined}` belief would never match a real\n * measurement, so every later fit would re-send a resize the PTY already has. */\ninterface CreateTerminalResponse {\n terminal: { slot: number; cols?: number; rows?: number };\n}\n\n/** `cols`/`rows` echo the PTY's real size; both optional so an older daemon that omits them is\n * handled without a version check (the client falls back to what it asked for). */\ninterface SubscribeTerminalResponse {\n cols?: number;\n rows?: number;\n}\n\n/** Status surface for the attach overlay (feature-panels-ui.md § Terminal pane → States). */\ninterface TerminalStatus {\n isAttaching: boolean;\n error: string | null;\n}\n\n/** Input typed before a slot exists is queued here, bounded, and flushed once the subscription\n * attaches successfully (feature-panels-ui.md § Input/keys). Cleared (not flushed) on a\n * subscribe error — a failed attach has nowhere correct to send queued bytes.\n *\n * Bounded in **bytes**, not chunks: one chunk is one `onData` payload, which for a paste is the\n * whole clipboard. A chunk-count cap would let 256 multi-megabyte pastes sit in memory. */\nconst MAX_PENDING_INPUT_BYTES = 64 * 1024;\n\nconst textEncoder = new TextEncoder();\n\n// One TerminalStreamRouter per daemon connection — multiple terminal tabs share it rather than\n// each opening its own frame demuxer over the same socket.\nconst routerByDaemon = new WeakMap<DaemonClient, TerminalStreamRouter>();\n\nfunction routerFor(daemon: DaemonClient): TerminalStreamRouter {\n let router = routerByDaemon.get(daemon);\n if (!router) {\n router = new TerminalStreamRouter(daemon);\n router.start();\n routerByDaemon.set(daemon, router);\n }\n return router;\n}\n\n/**\n * Whether this panel is the client's **size authority** for its PTY: it is on screen right now, in\n * the workspace the user is looking at, as its own pane's visible tab. Only an authority may send a\n * Resize frame (`terminals.md` § PTY size ownership — \"a passive observer never resizes what it is\n * only watching\"; a background tab or a tab in a non-active workspace is exactly that).\n *\n * Deliberately NOT gated on `focusedPaneId` (nor on real DOM focus). Pane focus is which pane\n * receives keystrokes; it is not what makes a rendered grid authoritative. Gating on it meant the\n * frame's fate depended on transient focus state at the exact moment a resize landed — a split with\n * a non-terminal tab, a workspace switch, or a restore each moved focus elsewhere while this\n * terminal was still the thing visibly rendering, so its real size went unreported and the shell\n * kept painting to a stale width (wrong grid, background color stopping short of the rendered\n * columns, mangled wrapping). Visibility is stable and is what the user is actually looking at.\n *\n * Reads live store state (`.getState()`, not a subscribed value) so a decision made synchronously\n * inside a native event handler or an rAF callback — either of which can run before React has\n * re-rendered this component — never sees a stale answer.\n */\nfunction isSizeAuthority(tabId: string): boolean {\n const tabState = useTabStore.getState();\n const tab = tabState.tabs.find((t) => t.id === tabId);\n if (!tab || tab.workspaceCwd !== tabState.activeWorkspaceCwd) return false;\n return isPaneActiveTab(useLayoutStore.getState().layouts[tab.workspaceCwd], tabId);\n}\n\n/** Dark palette matching the app's github-dark-ish default theme (theme/variants.ts \"dark\"). */\nconst TERMINAL_THEME = {\n background: \"#181b1a\",\n foreground: \"#fafafa\",\n cursor: \"#a2b4d7\",\n cursorAccent: \"#181b1a\",\n selectionBackground: \"rgba(255,255,255,0.18)\",\n black: \"#18181b\",\n red: \"#ef4444\",\n green: \"#22c55e\",\n yellow: \"#f59e0b\",\n blue: \"#3b82f6\",\n magenta: \"#a855f7\",\n cyan: \"#14b8a6\",\n white: \"#d4d4d8\",\n brightBlack: \"#52525b\",\n brightRed: \"#f87171\",\n brightGreen: \"#4ade80\",\n brightYellow: \"#fbbf24\",\n brightBlue: \"#60a5fa\",\n brightMagenta: \"#c084fc\",\n brightCyan: \"#2dd4bf\",\n brightWhite: \"#fafafa\",\n};\n\nexport function TerminalPanel({ tab }: TerminalPanelProps) {\n const data = tab.data as TerminalTabData;\n const client = useConnectionStore((s) => s.client);\n // Per-pane, not `=== activeTabId`: with splits this terminal can be on screen in one pane while\n // another pane holds the workspace-active tab, and it must refit when it appears either way.\n const isVisible = useIsTabVisible(tab.id);\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const terminalRef = useRef<Terminal | null>(null);\n const fitAddonRef = useRef<FitAddon | null>(null);\n const slotRef = useRef<number | null>(data.slot);\n // Current stream router for the slot's subscription — set by the subscription effect, read by\n // the emulator's onData/onResize handlers (which cannot close over it: they are attached once,\n // in the mount effect, and must keep working across a reconnect that swaps the router).\n const routerRef = useRef<TerminalStreamRouter | null>(null);\n const pendingInputRef = useRef<Uint8Array[]>([]);\n // Running total of `pendingInputRef`'s byte lengths, so the bound is checked without re-summing\n // the queue on every keystroke.\n const pendingInputBytesRef = useRef(0);\n // What this client believes the PTY's grid currently is — from `create_terminal_request`'s echo,\n // or from the last size it successfully sent. Used ONLY to dedupe (`shouldClaimSize`); it is\n // deliberately not a permission flag. `null` just means \"unknown\", which is the normal state of\n // a *restored* terminal whose PTY predates this client, and is precisely the case that most\n // needs a resize (that PTY is usually still at its 80×24 spawn default).\n const believedSizeRef = useRef<Grid | null>(null);\n // Set by the mount effect once `claimSize` exists, cleared on its cleanup, so the separate\n // visibility effect below can reuse the one claim path instead of duplicating its logic.\n const claimSizeRef = useRef<((next: Grid | null) => void) | null>(null);\n // Coalesced-refit scheduler state (sprint-052/task-004): `refitTimerRef` is the ~60ms trailing\n // debounce so a continuous gesture (divider drag, window resize) settles before fitting;\n // `refitRafRef` aligns the actual `fit()` to a paint frame; `isFittingRef` is the re-entrancy\n // guard so a `fit()` the scheduler performs cannot itself schedule another refit through the\n // `ResizeObserver` it may perturb. All three are refs, not effect-local state, because both the\n // emulator effect's `ResizeObserver` and the separate visibility effect below must share one\n // scheduler.\n const refitTimerRef = useRef<number | null>(null);\n const refitRafRef = useRef<number | null>(null);\n const isFittingRef = useRef(false);\n // Mirrors `use-checkout-status.ts`'s convention: kept in sync every render so the unmount-only\n // kill effect below always sends the CURRENT client, never a stale mount-time closure (e.g.\n // after a reconnect swaps in a new `PiStudioClient` instance).\n const clientRef = useRef(client);\n clientRef.current = client;\n\n const [slot, setSlot] = useState<number | null>(data.slot);\n const [status, setStatus] = useState<TerminalStatus>({ isAttaching: true, error: null });\n\n /**\n * Measure the panel's current grid and offer it to `claimSize`. The one shape every reconcile\n * point shares (post-fit, on focus, post-attach), so the measure→validate→claim sequence exists\n * once instead of three times drifting apart. Safe to call at any time: `claimSize` gates on\n * authority and dedupes, and an unmeasurable panel (hidden, 0×0) resolves to `null` and no-ops.\n */\n const measureAndClaim = () => {\n const proposed = fitAddonRef.current?.proposeDimensions();\n claimSizeRef.current?.(isMeasurable(proposed) ? proposed : null);\n };\n\n // Performs the coalesced fit, then reconciles. Not memoized — it closes over nothing but refs, so\n // a fresh function identity every render is harmless; whichever render's closure a pending\n // timer/rAF captured behaves identically to the current one.\n const performRefit = () => {\n refitRafRef.current = null;\n isFittingRef.current = true;\n fitAddonRef.current?.fit();\n // `fit()` alone is not enough to guarantee a claim. It only fires `onResize` when the grid\n // *changes*, so a panel that attached while hidden — measured 0×0, kept its constructor grid —\n // can become visible, fit to that same grid, and emit nothing, leaving the PTY at whatever it\n // was. Reconciling here covers that.\n measureAndClaim();\n // Hold the guard for one more frame: a `fit()`-induced box perturbation (if any) is reported\n // by `ResizeObserver` asynchronously, and has reliably arrived by the next frame.\n requestAnimationFrame(() => {\n isFittingRef.current = false;\n });\n };\n\n const requestRefit = () => {\n if (refitTimerRef.current !== null) window.clearTimeout(refitTimerRef.current);\n refitTimerRef.current = window.setTimeout(() => {\n refitTimerRef.current = null;\n if (refitRafRef.current !== null) return; // already scheduled this frame\n refitRafRef.current = requestAnimationFrame(performRefit);\n }, 60);\n };\n\n // ─── Emulator mount: independent of the slot ───────────────────────────────────────────────\n // Constructs xterm + FitAddon as soon as the container exists, attaches `onData`/`onResize`\n // BEFORE the first `fit()` — this is the root-cause fix (sprint-052): the one size-changing fit\n // of the panel's life used to fire before any resize listener existed, so no `Resize` frame was\n // ever sent and the PTY stayed at the 80×24 default forever. `onData`/`onResize` read the\n // current slot/router from refs (not a closure) because this effect has an empty deps array and\n // outlives every slot/client change; `claimSize` below is the size-claim logic behind\n // `sendResize` this ordering fix exists to make deliverable at all.\n useEffect(() => {\n // Captured once: the cleanup below must detach from the same element it attached to, and reading\n // the ref again at teardown would be reading it after React may have already nulled it.\n const container = containerRef.current;\n if (!container) return;\n\n const terminal = new Terminal({\n cursorBlink: true,\n fontFamily: \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace\",\n // An absolute px number, not a CSS var: xterm measures a cell from the computed style of its\n // own element and cannot resolve a var it was never given. The root font-size is left at the\n // browser default, so a rung's px value renders 1:1. (xterm 5.x renders to the DOM by\n // default — canvas/WebGL are addons this app does not load.)\n fontSize: baseFontSize.sm,\n scrollback: 5000,\n theme: TERMINAL_THEME,\n allowProposedApi: true,\n });\n const fitAddon = new FitAddon();\n terminal.loadAddon(fitAddon);\n terminal.open(container);\n\n // The one and only place a `Resize` frame originates (`terminals.md` § PTY size ownership).\n // Two independent gates, which an earlier revision wrongly fused into a single \"have I claimed\n // before?\" flag: `isSizeAuthority` is *permission* (am I the visible renderer?), and\n // `shouldClaimSize` is *validity + dedupe* against what we believe the PTY already is. A\n // restored terminal believes nothing, so its first real measurement always reports — which is\n // the whole point: its PTY is still at the 80×24 spawn default.\n const claimSize = (next: Grid | null) => {\n if (!isSizeAuthority(tab.id)) return;\n const currentSlot = slotRef.current;\n const router = routerRef.current;\n if (currentSlot === null || !router) return;\n if (!shouldClaimSize(next, believedSizeRef.current)) return;\n believedSizeRef.current = next;\n router.sendResize(currentSlot, next.rows, next.cols);\n };\n\n const dataDisposable = terminal.onData((chunk) => {\n const currentSlot = slotRef.current;\n const router = routerRef.current;\n if (currentSlot === null || !router) {\n // Pre-slot keystroke: queue it (bounded) rather than dropping it silently — the\n // subscription effect flushes this once it attaches successfully.\n const bytes = textEncoder.encode(chunk);\n if (pendingInputBytesRef.current + bytes.length <= MAX_PENDING_INPUT_BYTES) {\n pendingInputRef.current.push(bytes);\n pendingInputBytesRef.current += bytes.length;\n }\n return;\n }\n router.sendInput(currentSlot, textEncoder.encode(chunk));\n });\n // Every genuine grid change funnels through here: `FitAddon.fit()` only calls\n // `terminal.resize()` when the dimensions actually change, so this fires for window resizes,\n // divider drags, splits/collapses and font changes, but not for a refit that lands on the same\n // grid. `claimSize`'s authority gate is what keeps a background/other-workspace panel silent.\n const resizeDisposable = terminal.onResize(({ cols, rows }) => {\n claimSize({ cols, rows });\n });\n // Focus is not what confers authority (see `isSizeAuthority`), but it is a good moment to\n // reconcile: a click means the user is about to type, and a mismatched PTY width is what\n // mangles the line editor. `focusin`, not `focus`, because `focus` does not bubble and xterm\n // moves real focus to its own internal textarea.\n const handleFocus = () => {\n fitAddon.fit();\n measureAndClaim();\n };\n container.addEventListener(\"focusin\", handleFocus);\n\n // First fit runs AFTER both handlers are wired — see the effect comment above.\n fitAddon.fit();\n\n terminalRef.current = terminal;\n fitAddonRef.current = fitAddon;\n claimSizeRef.current = claimSize;\n\n // Coalesced (task-004): the observer only requests a refit; `requestRefit`'s trailing debounce\n // + rAF alignment is what actually calls `fit()`, so a continuous gesture (divider drag,\n // window resize) produces one fit()+claim at rest instead of one per intermediate frame,\n // eliminating the flicker/SIGWINCH-storm a synchronous fit-per-callback used to cause. Two\n // guards on top: `isFittingRef` skips an echo from our own scheduled fit() (see\n // `performRefit`), and a zero-size entry (hidden panel) is skipped without even debouncing —\n // reading it off the entry avoids forcing an extra layout `getBoundingClientRect()` would.\n const resizeObserver = new ResizeObserver((entries) => {\n if (isFittingRef.current) return;\n const entry = entries[0];\n if (entry && (entry.contentRect.width === 0 || entry.contentRect.height === 0)) return;\n requestRefit();\n });\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n if (refitTimerRef.current !== null) window.clearTimeout(refitTimerRef.current);\n if (refitRafRef.current !== null) cancelAnimationFrame(refitRafRef.current);\n container.removeEventListener(\"focusin\", handleFocus);\n dataDisposable.dispose();\n resizeDisposable.dispose();\n terminal.dispose();\n terminalRef.current = null;\n fitAddonRef.current = null;\n claimSizeRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ─── Slot lifecycle: create once, persist onto the tab so re-opening reuses it ─────────────\n // React StrictMode double-invokes effects in dev: mount → cleanup → remount, synchronously,\n // on the SAME component instance (refs/state persist across all three phases — this is not\n // three separate mounts). Two things must hold across that: (1) the request fires exactly\n // once, and (2) whether to APPLY the eventual response is decided by whether the component is\n // mounted at RESPONSE time, not by a flag captured at REQUEST time.\n //\n // `requestStartedRef` gives (1): set the instant the request fires and never reset, so the\n // phantom-mount's cleanup-then-remount sees it's already in flight and never fires a second\n // `create_terminal_request` (this is what previously spawned two real PTYs from one Ctrl+T).\n //\n // `isMountedRef` gives (2): flipped true at the START of every effect invocation and false in\n // every cleanup, so it always reflects the LATEST phase. StrictMode's remount happens\n // synchronously, before the request's promise can possibly settle, so by response time\n // `isMountedRef.current` is back to `true` for a StrictMode phantom (correctly applies the\n // slot) — but stays `false` for a genuine fast real close (correctly kills the orphaned PTY\n // instead of leaking it or, as the previous buggy version did, killing a terminal that was\n // never actually torn down).\n const isMountedRef = useRef(false);\n const requestStartedRef = useRef(false);\n useEffect(() => {\n isMountedRef.current = true;\n if (!client || slotRef.current !== null || requestStartedRef.current) {\n return () => {\n isMountedRef.current = false;\n };\n }\n requestStartedRef.current = true;\n\n const cwd = data.cwd || \"~\";\n const proposed = fitAddonRef.current?.proposeDimensions();\n const grid: Grid | null = isMeasurable(proposed) ? proposed : null;\n\n void client.connection\n .request<CreateTerminalResponse>(\"create_terminal_request\", {\n workspaceId: \"\",\n cwd,\n ...(grid ? { cols: grid.cols, rows: grid.rows } : {}),\n })\n .then((res) => {\n const created = res.terminal.slot;\n if (!isMountedRef.current) {\n // A real close happened with no remount after it — kill the PTY that finished\n // spawning after the tab was already gone, instead of leaking it.\n void client.connection\n .request(\"kill_terminal_request\", { slot: created })\n .catch(() => {});\n return;\n }\n slotRef.current = created;\n setSlot(created);\n useTabStore.getState().updateData(tab.id, { slot: created });\n // Record the daemon's echo, not our request: if it clamped or defaulted, our belief must\n // match what the PTY really is or the first dedupe check would wrongly suppress a needed\n // resize. Recorded even when we couldn't measure — the echo is then the 80×24 spawn default,\n // and knowing that beats believing nothing, since the panel's first real measurement will\n // differ and correctly report. An older daemon that echoes nothing leaves the belief `null`\n // (\"unknown\"), which is handled everywhere; seeding `{cols: undefined}` would not be.\n const echoed = { cols: res.terminal.cols, rows: res.terminal.rows };\n believedSizeRef.current = isMeasurable(echoed) ? echoed : null;\n })\n .catch((err: unknown) => {\n if (!isMountedRef.current) return;\n setStatus({ isAttaching: false, error: err instanceof Error ? err.message : String(err) });\n })\n .finally(() => {\n // Reset only after the promise settles — by then StrictMode's synchronous\n // mount→cleanup→remount window has long passed, so this can never reopen the\n // double-fire race. It DOES allow a legitimate retry (e.g. `client` changed because of\n // a reconnect after the first attempt failed) instead of leaving the tab stuck forever.\n requestStartedRef.current = false;\n });\n\n return () => {\n isMountedRef.current = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, tab.id]);\n\n // ─── True unmount only: kill the PTY server-side ───────────────────────────────────────────\n // Runs its cleanup exactly once, when this component itself unmounts (real tab close, per\n // `TabPanelHost`'s \"hidden but alive\" model — a tab switch never unmounts). An empty deps array\n // means React never re-runs the effect body itself; only the cleanup fires, and only on\n // unmount, so this never races the slot-creation effect above or double-kills on a client\n // change. Without this, every closed terminal tab leaked its PTY process forever.\n //\n // For a REATTACH (this tab opened with a non-null `data.slot` from the very first render —\n // `use-terminal-restore.ts`, or the create-effect above once it has resolved) `slotRef.current`\n // is non-null from the start, so StrictMode's synchronous mount→cleanup→remount phantom cycle\n // would fire this cleanup and kill the PTY immediately on mount — unlike a freshly created\n // terminal, where `slotRef.current` is still `null` during that same phantom window (see the\n // create-effect's own comment) and so never hits this path. Deferred via `setTimeout`, exactly\n // like the create-effect's response handler: by the time it fires, StrictMode's remount has\n // already flipped `isMountedRef` back to `true` if this was a phantom unmount, so the kill is\n // skipped; a genuine close never remounts, so `isMountedRef` stays `false` and the kill proceeds.\n useEffect(() => {\n return () => {\n const currentSlot = slotRef.current;\n if (currentSlot === null) return;\n setTimeout(() => {\n if (isMountedRef.current) return; // StrictMode remounted synchronously — not a real close\n void clientRef.current?.connection\n .request(\"kill_terminal_request\", { slot: currentSlot })\n .catch(() => {});\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ─── Stream subscription: keyed on [client, slot], independent of the emulator ─────────────\n // Split from the emulator mount so a reconnect (new `client`) re-subscribes without tearing\n // down and rebuilding xterm, which used to lose all scrollback on every reconnect.\n useEffect(() => {\n if (!client || slot === null) return;\n\n setStatus({ isAttaching: true, error: null });\n\n const router = routerFor(client.connection);\n routerRef.current = router;\n\n // `reset()`, not `clear()`: `clear()` only empties the viewport/scrollback, leaving the\n // emulator's modes (alt-screen, DECSTBM scroll margins, charset selection, wraparound/origin\n // modes, cursor visibility) carried over from whatever the previous stream left behind — a\n // snapshot replayed into stale modes renders confined to the wrong scroll region or with the\n // wrong charset (sprint-052/task-005; `terminals.md` § Restore / snapshot, tier 1). `reset()`\n // clears modes too, so the replay always lands on a clean slate. Shared by both handlers so\n // they cannot diverge — `onRestore` (tier 2, sprint-053) needs the identical treatment.\n const replay = (chunk: Uint8Array) => {\n terminalRef.current?.reset();\n terminalRef.current?.write(chunk);\n };\n const unsubscribeSlot = router.subscribeSlot(slot, {\n onOutput: (chunk) => terminalRef.current?.write(chunk),\n onSnapshot: replay,\n onRestore: replay,\n });\n\n let cancelled = false;\n // Send our measured grid WITH the subscribe request, not after it. The daemon resizes the PTY\n // before emitting the Snapshot, so a full-screen app (htop, vim) repaints at our width instead\n // of us replaying its 80-column byte stream into a much wider emulator and rendering scrambled\n // text. A client-side resize after attach is fundamentally too late: the snapshot is emitted\n // synchronously inside the daemon's subscribe, so those bytes are already on the wire.\n // Only send an authoritative measurement: a hidden panel measures 0×0 (`isMeasurable` false),\n // and it must not claim — its `performRefit` reconcile covers it once it becomes visible.\n const attachProposal = fitAddonRef.current?.proposeDimensions();\n const attachGrid: Grid | null =\n isMeasurable(attachProposal) && isSizeAuthority(tab.id) ? attachProposal : null;\n void client.connection\n .request<SubscribeTerminalResponse>(\"subscribe_terminal_request\", {\n slot,\n ...(attachGrid ? { cols: attachGrid.cols, rows: attachGrid.rows } : {}),\n })\n .then((res) => {\n if (cancelled) return;\n setStatus({ isAttaching: false, error: null });\n // Flush input queued while no slot/router existed yet (feature-panels-ui.md §\n // Input/keys: \"bounded pending queue flushed once attached + error-free\").\n const pending = pendingInputRef.current;\n pendingInputRef.current = [];\n pendingInputBytesRef.current = 0;\n for (const bytes of pending) router.sendInput(slot, bytes);\n // Seed belief from the daemon's echo of the PTY's real size — including the case where we\n // sent nothing, which is how a hidden panel learns what it is attached to instead of\n // guessing. Falls back to what we asked for if an older daemon doesn't echo, and to `null`\n // (\"unknown\") if we asked for nothing either.\n const echoed = { cols: res?.cols, rows: res?.rows };\n believedSizeRef.current = isMeasurable(echoed) ? echoed : attachGrid;\n // Reconcile anything that changed while the request was in flight (the pane could have been\n // resized, or this panel could have just become the authority). Deduped against the belief\n // just seeded, so it is a no-op in the common case.\n measureAndClaim();\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n // A failed attach has nowhere correct to send queued bytes — drop rather than flush.\n pendingInputRef.current = [];\n pendingInputBytesRef.current = 0;\n setStatus({ isAttaching: false, error: err instanceof Error ? err.message : String(err) });\n });\n\n return () => {\n cancelled = true;\n unsubscribeSlot();\n routerRef.current = null;\n void client.connection.request(\"unsubscribe_terminal_request\", { slot }).catch(() => {});\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, slot]);\n\n // ─── Re-fit whenever this tab becomes visible ─────────────────────────────────────────────\n // The `ResizeObserver` above catches divider drags and window resizes on its own (it observes\n // this panel's own box, which is what `TabPanelHost` sizes per pane); this covers the\n // hidden → visible transition, where the box was 0×0 while `display:none`. Routed through\n // `requestRefit` rather than fitting directly so it shares the same coalescing — a tab switch\n // during/adjacent to a layout gesture doesn't add a second immediate fit. No explicit claim\n // here: the refit's `fit()` fires `onResize` if the grid really changed, and that is the one\n // claim path. This is what makes a workspace switch self-correcting — the newly visible panel\n // becomes the size authority and its first non-zero measurement reports itself.\n useEffect(() => {\n if (!isVisible) return;\n requestRefit();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isVisible]);\n\n return (\n <div className={styles.wrap}>\n <div ref={containerRef} className={styles.terminal} />\n {status.error ? (\n <div className={styles.statusOverlay}>Terminal error: {status.error}</div>\n ) : status.isAttaching ? (\n <div className={styles.statusOverlay}>\n <Spinner size=\"sm\" /> Starting terminal…\n </div>\n ) : null}\n </div>\n );\n}\n"],"names":["TerminalStreamRouter","daemon","__publicField","frame","_a","slot","subscriber","data","encodeTerminalFrame","rows","cols","_b","_c","MIN_COLS","MIN_ROWS","isMeasurable","proposed","sameGrid","a","b","shouldClaimSize","next","believed","MAX_PENDING_INPUT_BYTES","textEncoder","routerByDaemon","routerFor","router","isSizeAuthority","tabId","tabState","useTabStore","tab","t","isPaneActiveTab","useLayoutStore","TERMINAL_THEME","TerminalPanel","client","useConnectionStore","s","isVisible","useIsTabVisible","containerRef","useRef","terminalRef","fitAddonRef","slotRef","routerRef","pendingInputRef","pendingInputBytesRef","believedSizeRef","claimSizeRef","refitTimerRef","refitRafRef","isFittingRef","clientRef","setSlot","useState","status","setStatus","measureAndClaim","performRefit","requestRefit","useEffect","container","terminal","Terminal","baseFontSize","fitAddon","FitAddon","claimSize","currentSlot","dataDisposable","chunk","bytes","resizeDisposable","handleFocus","resizeObserver","entries","entry","isMountedRef","requestStartedRef","cwd","grid","res","created","echoed","err","replay","unsubscribeSlot","cancelled","attachProposal","attachGrid","pending","jsxs","styles","jsx","Spinner"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{r as b,j as r}from"./vendor-react-CWo7Do4s.js";import{r as k,E as d,S as u,B as w,P as h}from"./index-DM5KSYdn.js";import{u as S,F as _,C as y}from"./CodeView-BG2-voMV.js";import{u as U}from"./vendor-query-BGIh-vlp.js";import{u as p}from"./use-file-download-BOCZENOG.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 B(n){const{enabled:s,download:e,decode:o}=n;return{isLoading:s&&(e.isLoading||e.hasObjectUrl&&o.isLoading),isError:e.isError||o.isError,error:e.isError?e.error:o.error,data:o.data}}function F(n,s=!0){var a;const e=p(n,s),o=(a=e.data)==null?void 0:a.objectUrl,t=U({queryKey:k.fileText(n,o??null),queryFn:async()=>({content:await(await fetch(o)).text()}),enabled:s&&!!o});return B({enabled:s,download:{isLoading:e.isLoading,isError:e.isError,error:e.error,hasObjectUrl:!!o},decode:{isLoading:t.isLoading,isError:t.isError,error:t.error,data:t.data}})}function N(n){const{maxDisplayBytes:s,inline:e,streamed:o,download:t}=n;if(e.isLoading)return{kind:"loading"};if(e.tooLarge){const{size:a}=e.tooLarge;return a>s?{kind:"too-large",size:a,maxDisplayBytes:s,downloading:t.requested&&t.isLoading,downloadUrl:t.objectUrl,downloadName:t.fileName}:o.isLoading?{kind:"streaming",size:a}:o.isError?{kind:"stream-error",size:a,message:o.errorMessage??"stream failed"}:o.content!==null?{kind:"streamed",size:a,content:o.content}:{kind:"loading"}}return e.isError?{kind:"error",message:e.errorMessage??"unknown error"}:e.content!==null?{kind:"inline",content:e.content}:{kind:"loading"}}const D="_note_1g5yz_1",M="_body_1g5yz_9",T="_tooLarge_1g5yz_13",m={note:D,body:M,tooLarge:T},E=30*1024*1024;function l(n){return`${(n/(1024*1024)).toFixed(1)} MB`}function Q({path:n}){var f,x,j,L;const s=S(n),e=s.error instanceof _?s.error:null,o=e!==null&&e.size<=E,t=F(n,o),[a,g]=b.useState(!1),c=p(n,a),i=N({maxDisplayBytes:E,inline:{isLoading:s.isLoading,isError:s.isError,tooLarge:e?{size:e.size}:null,errorMessage:s.error instanceof Error?s.error.message:null,content:((f=s.data)==null?void 0:f.content)??null},streamed:{isLoading:t.isLoading,isError:t.isError,errorMessage:t.error instanceof Error?t.error.message:null,content:((x=t.data)==null?void 0:x.content)??null},download:{requested:a,isLoading:c.isLoading,objectUrl:((j=c.data)==null?void 0:j.objectUrl)??null,fileName:((L=c.data)==null?void 0:L.fileName)??null}});switch(i.kind){case"loading":return r.jsxs(d,{children:[r.jsx(u,{size:"sm"})," Loading..."]});case"inline":return r.jsx(y,{path:n,content:i.content});case"streaming":return r.jsxs(d,{children:[r.jsx(u,{size:"sm"})," Streaming ",l(i.size)," file..."]});case"stream-error":return r.jsxs(d,{children:["Error: ",i.message]});case"streamed":return r.jsxs(h,{children:[r.jsxs("div",{className:m.note,children:[l(i.size)," file streamed"]}),r.jsx("div",{className:m.body,children:r.jsx(y,{path:n,content:i.content})})]});case"too-large":{const z=n.split("/").pop()||n;return r.jsx(d,{children:r.jsxs("div",{className:m.tooLarge,children:[r.jsxs("div",{children:[l(i.size)," — too large to display (display ceiling is"," ",l(i.maxDisplayBytes),")."]}),i.downloading?r.jsx(u,{size:"sm"}):i.downloadUrl?r.jsx("a",{href:i.downloadUrl,download:i.downloadName||z,children:r.jsx(w,{size:"sm",children:"Save file"})}):r.jsx(w,{size:"sm",onClick:()=>g(!0),children:"Download"})]})})}case"error":return r.jsxs(d,{children:["Error: ",i.message]})}}export{E as MAX_DISPLAY_BYTES,Q as TextViewer};
2
- //# sourceMappingURL=TextViewer-DdViYQah.js.map
1
+ import{r as b,j as r}from"./vendor-react-CWo7Do4s.js";import{r as k,E as d,S as u,B as w,P as h}from"./index-DonKPQN3.js";import{u as S,F as _,C as y}from"./CodeView-vzOAirYe.js";import{u as U}from"./vendor-query-BGIh-vlp.js";import{u as p}from"./use-file-download-C2Lrmbfe.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 B(n){const{enabled:s,download:e,decode:o}=n;return{isLoading:s&&(e.isLoading||e.hasObjectUrl&&o.isLoading),isError:e.isError||o.isError,error:e.isError?e.error:o.error,data:o.data}}function F(n,s=!0){var a;const e=p(n,s),o=(a=e.data)==null?void 0:a.objectUrl,t=U({queryKey:k.fileText(n,o??null),queryFn:async()=>({content:await(await fetch(o)).text()}),enabled:s&&!!o});return B({enabled:s,download:{isLoading:e.isLoading,isError:e.isError,error:e.error,hasObjectUrl:!!o},decode:{isLoading:t.isLoading,isError:t.isError,error:t.error,data:t.data}})}function N(n){const{maxDisplayBytes:s,inline:e,streamed:o,download:t}=n;if(e.isLoading)return{kind:"loading"};if(e.tooLarge){const{size:a}=e.tooLarge;return a>s?{kind:"too-large",size:a,maxDisplayBytes:s,downloading:t.requested&&t.isLoading,downloadUrl:t.objectUrl,downloadName:t.fileName}:o.isLoading?{kind:"streaming",size:a}:o.isError?{kind:"stream-error",size:a,message:o.errorMessage??"stream failed"}:o.content!==null?{kind:"streamed",size:a,content:o.content}:{kind:"loading"}}return e.isError?{kind:"error",message:e.errorMessage??"unknown error"}:e.content!==null?{kind:"inline",content:e.content}:{kind:"loading"}}const D="_note_1g5yz_1",M="_body_1g5yz_9",T="_tooLarge_1g5yz_13",m={note:D,body:M,tooLarge:T},E=30*1024*1024;function l(n){return`${(n/(1024*1024)).toFixed(1)} MB`}function Q({path:n}){var f,x,j,L;const s=S(n),e=s.error instanceof _?s.error:null,o=e!==null&&e.size<=E,t=F(n,o),[a,g]=b.useState(!1),c=p(n,a),i=N({maxDisplayBytes:E,inline:{isLoading:s.isLoading,isError:s.isError,tooLarge:e?{size:e.size}:null,errorMessage:s.error instanceof Error?s.error.message:null,content:((f=s.data)==null?void 0:f.content)??null},streamed:{isLoading:t.isLoading,isError:t.isError,errorMessage:t.error instanceof Error?t.error.message:null,content:((x=t.data)==null?void 0:x.content)??null},download:{requested:a,isLoading:c.isLoading,objectUrl:((j=c.data)==null?void 0:j.objectUrl)??null,fileName:((L=c.data)==null?void 0:L.fileName)??null}});switch(i.kind){case"loading":return r.jsxs(d,{children:[r.jsx(u,{size:"sm"})," Loading..."]});case"inline":return r.jsx(y,{path:n,content:i.content});case"streaming":return r.jsxs(d,{children:[r.jsx(u,{size:"sm"})," Streaming ",l(i.size)," file..."]});case"stream-error":return r.jsxs(d,{children:["Error: ",i.message]});case"streamed":return r.jsxs(h,{children:[r.jsxs("div",{className:m.note,children:[l(i.size)," file streamed"]}),r.jsx("div",{className:m.body,children:r.jsx(y,{path:n,content:i.content})})]});case"too-large":{const z=n.split("/").pop()||n;return r.jsx(d,{children:r.jsxs("div",{className:m.tooLarge,children:[r.jsxs("div",{children:[l(i.size)," — too large to display (display ceiling is"," ",l(i.maxDisplayBytes),")."]}),i.downloading?r.jsx(u,{size:"sm"}):i.downloadUrl?r.jsx("a",{href:i.downloadUrl,download:i.downloadName||z,children:r.jsx(w,{size:"sm",children:"Save file"})}):r.jsx(w,{size:"sm",onClick:()=>g(!0),children:"Download"})]})})}case"error":return r.jsxs(d,{children:["Error: ",i.message]})}}export{E as MAX_DISPLAY_BYTES,Q as TextViewer};
2
+ //# sourceMappingURL=TextViewer-C7U5fAYp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextViewer-DdViYQah.js","sources":["../../../src/hooks/file-text-state.ts","../../../src/hooks/use-file-text.ts","../../../src/features/files/text-viewer-state.ts","../../../src/features/files/TextViewer.tsx"],"sourcesContent":["/**\n * Pure query-state merge for `use-file-text` (task-009), kept DOM/React-free so the\n * loading/error/data composition across the two dependent queries it wraps (download → decode)\n * is unit-testable directly, without `renderHook` (no jsdom test environment in this repo).\n */\n\nexport interface FileTextResult {\n content: string;\n}\n\nexport interface FileTextMergeInputs {\n enabled: boolean;\n download: { isLoading: boolean; isError: boolean; error: unknown; hasObjectUrl: boolean };\n decode: {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n };\n}\n\nexport interface FileTextQueryState {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n}\n\nexport function mergeFileTextState(input: FileTextMergeInputs): FileTextQueryState {\n const { enabled, download, decode } = input;\n return {\n isLoading: enabled && (download.isLoading || (download.hasObjectUrl && decode.isLoading)),\n isError: download.isError || decode.isError,\n error: download.isError ? download.error : decode.error,\n data: decode.data,\n };\n}\n","/**\n * `use-file-text` — tier-2 fallback for `TextViewer` (task-009): decodes a file's bytes to UTF-8\n * text via the already-uncapped chunked binary download path (`useFileDownload` →\n * `file_download_token_request`/`file_download_request`), for files over `file_read_request`'s\n * inline cap (`MAX_INLINE_FILE_READ_BYTES`, `packages/server/src/files/limits.ts`). Same\n * transport the molecule viewer uses for its own (binary-safe) source — no new RPC, no new\n * transport, just a text decode over the resulting blob.\n *\n * Note the daemon asymmetry documented on `useFileDownload`'s own RPCs: the download path is\n * registered only in the production bootstrap (`bootstrap.ts`), not `dev-bootstrap.ts`. Under\n * `npm run dev:daemon` a file above the inline cap fails this fetch rather than rendering — that\n * surfaces as the normal error state below, which is the intended behavior (see task-009 notes).\n *\n * The loading/error/data merge across the two dependent queries (download → decode) is the pure,\n * DOM-free `mergeFileTextState` (`file-text-state.ts`) — this hook is a thin wrapper feeding it\n * live query state.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { useFileDownload } from \"./use-file-download.js\";\nimport { mergeFileTextState, type FileTextResult } from \"./file-text-state.js\";\n\nexport type { FileTextResult };\n\nexport function useFileText(path: string, enabled = true) {\n const download = useFileDownload(path, enabled);\n const objectUrl = download.data?.objectUrl;\n\n const decode = useQuery({\n queryKey: rpcKeys.fileText(path, objectUrl ?? null),\n queryFn: async (): Promise<FileTextResult> => {\n const content = await (await fetch(objectUrl as string)).text();\n return { content };\n },\n enabled: enabled && Boolean(objectUrl),\n });\n\n return mergeFileTextState({\n enabled,\n download: {\n isLoading: download.isLoading,\n isError: download.isError,\n error: download.error,\n hasObjectUrl: Boolean(objectUrl),\n },\n decode: {\n isLoading: decode.isLoading,\n isError: decode.isError,\n error: decode.error,\n data: decode.data,\n },\n });\n}\n","/**\n * Pure tier-selection logic for `TextViewer` (task-009), kept in its own DOM-free module so the\n * three-state branch (inline / streamed / download-only) is unit-testable without mounting\n * CodeMirror or React — mirrors `molecule-reload.ts`'s `shouldApplyRefresh` extraction for the\n * same reason: this repo has no jsdom test environment configured anywhere.\n */\n\nexport type TextViewerState =\n | { kind: \"loading\" }\n | { kind: \"inline\"; content: string }\n | { kind: \"streaming\"; size: number }\n | { kind: \"streamed\"; size: number; content: string }\n | { kind: \"stream-error\"; size: number; message: string }\n | {\n kind: \"too-large\";\n size: number;\n maxDisplayBytes: number;\n downloading: boolean;\n downloadUrl: string | null;\n downloadName: string | null;\n }\n | { kind: \"error\"; message: string };\n\nexport interface TextViewerInputs {\n maxDisplayBytes: number;\n inline: {\n isLoading: boolean;\n isError: boolean;\n /** `{ size }` when the failure was specifically `FileTooLargeError`; `null` for a generic\n * read failure (network/permission/etc). */\n tooLarge: { size: number } | null;\n errorMessage: string | null;\n content: string | null;\n };\n streamed: {\n isLoading: boolean;\n isError: boolean;\n errorMessage: string | null;\n content: string | null;\n };\n download: {\n requested: boolean;\n isLoading: boolean;\n objectUrl: string | null;\n fileName: string | null;\n };\n}\n\n/** Selects exactly one `TextViewerState` from the current query states. Pure — no hooks, no I/O. */\nexport function selectTextViewerState(input: TextViewerInputs): TextViewerState {\n const { maxDisplayBytes, inline, streamed, download } = input;\n\n if (inline.isLoading) return { kind: \"loading\" };\n\n if (inline.tooLarge) {\n const { size } = inline.tooLarge;\n if (size > maxDisplayBytes) {\n return {\n kind: \"too-large\",\n size,\n maxDisplayBytes,\n downloading: download.requested && download.isLoading,\n downloadUrl: download.objectUrl,\n downloadName: download.fileName,\n };\n }\n if (streamed.isLoading) return { kind: \"streaming\", size };\n if (streamed.isError) {\n return { kind: \"stream-error\", size, message: streamed.errorMessage ?? \"stream failed\" };\n }\n if (streamed.content !== null) return { kind: \"streamed\", size, content: streamed.content };\n return { kind: \"loading\" };\n }\n\n if (inline.isError) {\n return { kind: \"error\", message: inline.errorMessage ?? \"unknown error\" };\n }\n\n if (inline.content !== null) return { kind: \"inline\", content: inline.content };\n return { kind: \"loading\" };\n}\n","/**\n * TextViewer — the default viewer for source/text files: fetches the UTF-8 preview via\n * `useFileRead` and renders it through `CodeView` (line gutter + Shiki highlighting). Registered\n * in `viewer-registry.ts` as the fallback for any file not claimed by a more specific viewer.\n *\n * Three size tiers (task-009 — raising `file_read_request`'s old 512 KiB ceiling):\n * 1. `size <= MAX_INLINE_FILE_READ_BYTES` (server-side, 5 MiB) — the `useFileRead` JSON round\n * trip above, unchanged.\n * 2. `MAX_INLINE_FILE_READ_BYTES < size <= MAX_DISPLAY_BYTES` (30 MiB) — transparently refetch via\n * the uncapped chunked binary download path (`use-file-text.ts`) and render the same\n * `CodeView`, with a muted note that the file was streamed rather than read inline.\n * 3. `size > MAX_DISPLAY_BYTES` — a terminal state: no render attempt, just the size, why, and a\n * download action (`BinaryFallbackViewer`'s existing on-demand download pattern).\n *\n * Which tier applies is the pure, DOM-free `selectTextViewerState` (`text-viewer-state.ts`) — this\n * component is a thin switch-render over it.\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 { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileRead, FileTooLargeError } from \"@pi-studio-ui/hooks/use-file-read.js\";\nimport { useFileText } from \"@pi-studio-ui/hooks/use-file-text.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { CodeView } from \"./CodeView.js\";\nimport { selectTextViewerState } from \"./text-viewer-state.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport textStyles from \"./TextViewer.module.css\";\n\n/** Display ceiling above the inline cap. `CodeView`'s CodeMirror instance enables\n * `EditorView.lineWrapping`, which makes it measure line heights across the whole document —\n * well past interactive above this size, so files this large get a terminal download-only state\n * instead of an attempted render. Deliberately not configurable (task-009 notes). */\nexport const MAX_DISPLAY_BYTES = 30 * 1024 * 1024;\n\nfunction formatMegabytes(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function TextViewer({ path }: ViewerProps) {\n const inline = useFileRead(path);\n const tooLarge = inline.error instanceof FileTooLargeError ? inline.error : null;\n const displayable = tooLarge !== null && tooLarge.size <= MAX_DISPLAY_BYTES;\n\n const streamed = useFileText(path, displayable);\n const [downloadRequested, setDownloadRequested] = useState(false);\n const download = useFileDownload(path, downloadRequested);\n\n const state = selectTextViewerState({\n maxDisplayBytes: MAX_DISPLAY_BYTES,\n inline: {\n isLoading: inline.isLoading,\n isError: inline.isError,\n tooLarge: tooLarge ? { size: tooLarge.size } : null,\n errorMessage: inline.error instanceof Error ? inline.error.message : null,\n content: inline.data?.content ?? null,\n },\n streamed: {\n isLoading: streamed.isLoading,\n isError: streamed.isError,\n errorMessage: streamed.error instanceof Error ? streamed.error.message : null,\n content: streamed.data?.content ?? null,\n },\n download: {\n requested: downloadRequested,\n isLoading: download.isLoading,\n objectUrl: download.data?.objectUrl ?? null,\n fileName: download.data?.fileName ?? null,\n },\n });\n\n switch (state.kind) {\n case \"loading\":\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n );\n case \"inline\":\n return <CodeView path={path} content={state.content} />;\n case \"streaming\":\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Streaming {formatMegabytes(state.size)} file...\n </EmptyState>\n );\n case \"stream-error\":\n return <EmptyState>Error: {state.message}</EmptyState>;\n case \"streamed\":\n return (\n <Panel>\n <div className={textStyles.note}>{formatMegabytes(state.size)} file streamed</div>\n <div className={textStyles.body}>\n <CodeView path={path} content={state.content} />\n </div>\n </Panel>\n );\n case \"too-large\": {\n const name = path.split(\"/\").pop() || path;\n return (\n <EmptyState>\n <div className={textStyles.tooLarge}>\n <div>\n {formatMegabytes(state.size)} — too large to display (display ceiling is{\" \"}\n {formatMegabytes(state.maxDisplayBytes)}).\n </div>\n {state.downloading ? (\n <Spinner size=\"sm\" />\n ) : state.downloadUrl ? (\n <a href={state.downloadUrl} download={state.downloadName || name}>\n <Button size=\"sm\">Save file</Button>\n </a>\n ) : (\n <Button size=\"sm\" onClick={() => setDownloadRequested(true)}>\n Download\n </Button>\n )}\n </div>\n </EmptyState>\n );\n }\n case \"error\":\n return <EmptyState>Error: {state.message}</EmptyState>;\n }\n}\n"],"names":["mergeFileTextState","input","enabled","download","decode","useFileText","path","useFileDownload","objectUrl","_a","useQuery","rpcKeys","selectTextViewerState","maxDisplayBytes","inline","streamed","size","MAX_DISPLAY_BYTES","formatMegabytes","bytes","TextViewer","useFileRead","tooLarge","FileTooLargeError","displayable","downloadRequested","setDownloadRequested","useState","state","_b","_c","_d","EmptyState","jsx","Spinner","CodeView","Panel","jsxs","textStyles","name","Button"],"mappings":"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"}
1
+ {"version":3,"file":"TextViewer-C7U5fAYp.js","sources":["../../../src/hooks/file-text-state.ts","../../../src/hooks/use-file-text.ts","../../../src/features/files/text-viewer-state.ts","../../../src/features/files/TextViewer.tsx"],"sourcesContent":["/**\n * Pure query-state merge for `use-file-text` (task-009), kept DOM/React-free so the\n * loading/error/data composition across the two dependent queries it wraps (download → decode)\n * is unit-testable directly, without `renderHook` (no jsdom test environment in this repo).\n */\n\nexport interface FileTextResult {\n content: string;\n}\n\nexport interface FileTextMergeInputs {\n enabled: boolean;\n download: { isLoading: boolean; isError: boolean; error: unknown; hasObjectUrl: boolean };\n decode: {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n };\n}\n\nexport interface FileTextQueryState {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n}\n\nexport function mergeFileTextState(input: FileTextMergeInputs): FileTextQueryState {\n const { enabled, download, decode } = input;\n return {\n isLoading: enabled && (download.isLoading || (download.hasObjectUrl && decode.isLoading)),\n isError: download.isError || decode.isError,\n error: download.isError ? download.error : decode.error,\n data: decode.data,\n };\n}\n","/**\n * `use-file-text` — tier-2 fallback for `TextViewer` (task-009): decodes a file's bytes to UTF-8\n * text via the already-uncapped chunked binary download path (`useFileDownload` →\n * `file_download_token_request`/`file_download_request`), for files over `file_read_request`'s\n * inline cap (`MAX_INLINE_FILE_READ_BYTES`, `packages/server/src/files/limits.ts`). Same\n * transport the molecule viewer uses for its own (binary-safe) source — no new RPC, no new\n * transport, just a text decode over the resulting blob.\n *\n * Note the daemon asymmetry documented on `useFileDownload`'s own RPCs: the download path is\n * registered only in the production bootstrap (`bootstrap.ts`), not `dev-bootstrap.ts`. Under\n * `npm run dev:daemon` a file above the inline cap fails this fetch rather than rendering — that\n * surfaces as the normal error state below, which is the intended behavior (see task-009 notes).\n *\n * The loading/error/data merge across the two dependent queries (download → decode) is the pure,\n * DOM-free `mergeFileTextState` (`file-text-state.ts`) — this hook is a thin wrapper feeding it\n * live query state.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { useFileDownload } from \"./use-file-download.js\";\nimport { mergeFileTextState, type FileTextResult } from \"./file-text-state.js\";\n\nexport type { FileTextResult };\n\nexport function useFileText(path: string, enabled = true) {\n const download = useFileDownload(path, enabled);\n const objectUrl = download.data?.objectUrl;\n\n const decode = useQuery({\n queryKey: rpcKeys.fileText(path, objectUrl ?? null),\n queryFn: async (): Promise<FileTextResult> => {\n const content = await (await fetch(objectUrl as string)).text();\n return { content };\n },\n enabled: enabled && Boolean(objectUrl),\n });\n\n return mergeFileTextState({\n enabled,\n download: {\n isLoading: download.isLoading,\n isError: download.isError,\n error: download.error,\n hasObjectUrl: Boolean(objectUrl),\n },\n decode: {\n isLoading: decode.isLoading,\n isError: decode.isError,\n error: decode.error,\n data: decode.data,\n },\n });\n}\n","/**\n * Pure tier-selection logic for `TextViewer` (task-009), kept in its own DOM-free module so the\n * three-state branch (inline / streamed / download-only) is unit-testable without mounting\n * CodeMirror or React — mirrors `molecule-reload.ts`'s `shouldApplyRefresh` extraction for the\n * same reason: this repo has no jsdom test environment configured anywhere.\n */\n\nexport type TextViewerState =\n | { kind: \"loading\" }\n | { kind: \"inline\"; content: string }\n | { kind: \"streaming\"; size: number }\n | { kind: \"streamed\"; size: number; content: string }\n | { kind: \"stream-error\"; size: number; message: string }\n | {\n kind: \"too-large\";\n size: number;\n maxDisplayBytes: number;\n downloading: boolean;\n downloadUrl: string | null;\n downloadName: string | null;\n }\n | { kind: \"error\"; message: string };\n\nexport interface TextViewerInputs {\n maxDisplayBytes: number;\n inline: {\n isLoading: boolean;\n isError: boolean;\n /** `{ size }` when the failure was specifically `FileTooLargeError`; `null` for a generic\n * read failure (network/permission/etc). */\n tooLarge: { size: number } | null;\n errorMessage: string | null;\n content: string | null;\n };\n streamed: {\n isLoading: boolean;\n isError: boolean;\n errorMessage: string | null;\n content: string | null;\n };\n download: {\n requested: boolean;\n isLoading: boolean;\n objectUrl: string | null;\n fileName: string | null;\n };\n}\n\n/** Selects exactly one `TextViewerState` from the current query states. Pure — no hooks, no I/O. */\nexport function selectTextViewerState(input: TextViewerInputs): TextViewerState {\n const { maxDisplayBytes, inline, streamed, download } = input;\n\n if (inline.isLoading) return { kind: \"loading\" };\n\n if (inline.tooLarge) {\n const { size } = inline.tooLarge;\n if (size > maxDisplayBytes) {\n return {\n kind: \"too-large\",\n size,\n maxDisplayBytes,\n downloading: download.requested && download.isLoading,\n downloadUrl: download.objectUrl,\n downloadName: download.fileName,\n };\n }\n if (streamed.isLoading) return { kind: \"streaming\", size };\n if (streamed.isError) {\n return { kind: \"stream-error\", size, message: streamed.errorMessage ?? \"stream failed\" };\n }\n if (streamed.content !== null) return { kind: \"streamed\", size, content: streamed.content };\n return { kind: \"loading\" };\n }\n\n if (inline.isError) {\n return { kind: \"error\", message: inline.errorMessage ?? \"unknown error\" };\n }\n\n if (inline.content !== null) return { kind: \"inline\", content: inline.content };\n return { kind: \"loading\" };\n}\n","/**\n * TextViewer — the default viewer for source/text files: fetches the UTF-8 preview via\n * `useFileRead` and renders it through `CodeView` (line gutter + Shiki highlighting). Registered\n * in `viewer-registry.ts` as the fallback for any file not claimed by a more specific viewer.\n *\n * Three size tiers (task-009 — raising `file_read_request`'s old 512 KiB ceiling):\n * 1. `size <= MAX_INLINE_FILE_READ_BYTES` (server-side, 5 MiB) — the `useFileRead` JSON round\n * trip above, unchanged.\n * 2. `MAX_INLINE_FILE_READ_BYTES < size <= MAX_DISPLAY_BYTES` (30 MiB) — transparently refetch via\n * the uncapped chunked binary download path (`use-file-text.ts`) and render the same\n * `CodeView`, with a muted note that the file was streamed rather than read inline.\n * 3. `size > MAX_DISPLAY_BYTES` — a terminal state: no render attempt, just the size, why, and a\n * download action (`BinaryFallbackViewer`'s existing on-demand download pattern).\n *\n * Which tier applies is the pure, DOM-free `selectTextViewerState` (`text-viewer-state.ts`) — this\n * component is a thin switch-render over it.\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 { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileRead, FileTooLargeError } from \"@pi-studio-ui/hooks/use-file-read.js\";\nimport { useFileText } from \"@pi-studio-ui/hooks/use-file-text.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { CodeView } from \"./CodeView.js\";\nimport { selectTextViewerState } from \"./text-viewer-state.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport textStyles from \"./TextViewer.module.css\";\n\n/** Display ceiling above the inline cap. `CodeView`'s CodeMirror instance enables\n * `EditorView.lineWrapping`, which makes it measure line heights across the whole document —\n * well past interactive above this size, so files this large get a terminal download-only state\n * instead of an attempted render. Deliberately not configurable (task-009 notes). */\nexport const MAX_DISPLAY_BYTES = 30 * 1024 * 1024;\n\nfunction formatMegabytes(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function TextViewer({ path }: ViewerProps) {\n const inline = useFileRead(path);\n const tooLarge = inline.error instanceof FileTooLargeError ? inline.error : null;\n const displayable = tooLarge !== null && tooLarge.size <= MAX_DISPLAY_BYTES;\n\n const streamed = useFileText(path, displayable);\n const [downloadRequested, setDownloadRequested] = useState(false);\n const download = useFileDownload(path, downloadRequested);\n\n const state = selectTextViewerState({\n maxDisplayBytes: MAX_DISPLAY_BYTES,\n inline: {\n isLoading: inline.isLoading,\n isError: inline.isError,\n tooLarge: tooLarge ? { size: tooLarge.size } : null,\n errorMessage: inline.error instanceof Error ? inline.error.message : null,\n content: inline.data?.content ?? null,\n },\n streamed: {\n isLoading: streamed.isLoading,\n isError: streamed.isError,\n errorMessage: streamed.error instanceof Error ? streamed.error.message : null,\n content: streamed.data?.content ?? null,\n },\n download: {\n requested: downloadRequested,\n isLoading: download.isLoading,\n objectUrl: download.data?.objectUrl ?? null,\n fileName: download.data?.fileName ?? null,\n },\n });\n\n switch (state.kind) {\n case \"loading\":\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n );\n case \"inline\":\n return <CodeView path={path} content={state.content} />;\n case \"streaming\":\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Streaming {formatMegabytes(state.size)} file...\n </EmptyState>\n );\n case \"stream-error\":\n return <EmptyState>Error: {state.message}</EmptyState>;\n case \"streamed\":\n return (\n <Panel>\n <div className={textStyles.note}>{formatMegabytes(state.size)} file streamed</div>\n <div className={textStyles.body}>\n <CodeView path={path} content={state.content} />\n </div>\n </Panel>\n );\n case \"too-large\": {\n const name = path.split(\"/\").pop() || path;\n return (\n <EmptyState>\n <div className={textStyles.tooLarge}>\n <div>\n {formatMegabytes(state.size)} — too large to display (display ceiling is{\" \"}\n {formatMegabytes(state.maxDisplayBytes)}).\n </div>\n {state.downloading ? (\n <Spinner size=\"sm\" />\n ) : state.downloadUrl ? (\n <a href={state.downloadUrl} download={state.downloadName || name}>\n <Button size=\"sm\">Save file</Button>\n </a>\n ) : (\n <Button size=\"sm\" onClick={() => setDownloadRequested(true)}>\n Download\n </Button>\n )}\n </div>\n </EmptyState>\n );\n }\n case \"error\":\n return <EmptyState>Error: {state.message}</EmptyState>;\n }\n}\n"],"names":["mergeFileTextState","input","enabled","download","decode","useFileText","path","useFileDownload","objectUrl","_a","useQuery","rpcKeys","selectTextViewerState","maxDisplayBytes","inline","streamed","size","MAX_DISPLAY_BYTES","formatMegabytes","bytes","TextViewer","useFileRead","tooLarge","FileTooLargeError","displayable","downloadRequested","setDownloadRequested","useState","state","_b","_c","_d","EmptyState","jsx","Spinner","CodeView","Panel","jsxs","textStyles","name","Button"],"mappings":"0eA4BO,SAASA,EAAmBC,EAAgD,CACjF,KAAM,CAAE,QAAAC,EAAS,SAAAC,EAAU,OAAAC,CAAA,EAAWH,EACtC,MAAO,CACL,UAAWC,IAAYC,EAAS,WAAcA,EAAS,cAAgBC,EAAO,WAC9E,QAASD,EAAS,SAAWC,EAAO,QACpC,MAAOD,EAAS,QAAUA,EAAS,MAAQC,EAAO,MAClD,KAAMA,EAAO,IAAA,CAEjB,CCXO,SAASC,EAAYC,EAAcJ,EAAU,GAAM,OACxD,MAAMC,EAAWI,EAAgBD,EAAMJ,CAAO,EACxCM,GAAYC,EAAAN,EAAS,OAAT,YAAAM,EAAe,UAE3BL,EAASM,EAAS,CACtB,SAAUC,EAAQ,SAASL,EAAME,GAAa,IAAI,EAClD,QAAS,UAEA,CAAE,QADO,MAAO,MAAM,MAAMA,CAAmB,GAAG,KAAA,CAChD,GAEX,QAASN,GAAW,EAAQM,CAAS,CACtC,EAED,OAAOR,EAAmB,CACxB,QAAAE,EACA,SAAU,CACR,UAAWC,EAAS,UACpB,QAASA,EAAS,QAClB,MAAOA,EAAS,MAChB,aAAc,EAAQK,CAAS,EAEjC,OAAQ,CACN,UAAWJ,EAAO,UAClB,QAASA,EAAO,QAChB,MAAOA,EAAO,MACd,KAAMA,EAAO,IAAA,CACf,CACD,CACH,CCJO,SAASQ,EAAsBX,EAA0C,CAC9E,KAAM,CAAE,gBAAAY,EAAiB,OAAAC,EAAQ,SAAAC,EAAU,SAAAZ,GAAaF,EAExD,GAAIa,EAAO,UAAW,MAAO,CAAE,KAAM,SAAA,EAErC,GAAIA,EAAO,SAAU,CACnB,KAAM,CAAE,KAAAE,GAASF,EAAO,SACxB,OAAIE,EAAOH,EACF,CACL,KAAM,YACN,KAAAG,EACA,gBAAAH,EACA,YAAaV,EAAS,WAAaA,EAAS,UAC5C,YAAaA,EAAS,UACtB,aAAcA,EAAS,QAAA,EAGvBY,EAAS,UAAkB,CAAE,KAAM,YAAa,KAAAC,CAAA,EAChDD,EAAS,QACJ,CAAE,KAAM,eAAgB,KAAAC,EAAM,QAASD,EAAS,cAAgB,eAAA,EAErEA,EAAS,UAAY,KAAa,CAAE,KAAM,WAAY,KAAAC,EAAM,QAASD,EAAS,OAAA,EAC3E,CAAE,KAAM,SAAA,CACjB,CAEA,OAAID,EAAO,QACF,CAAE,KAAM,QAAS,QAASA,EAAO,cAAgB,eAAA,EAGtDA,EAAO,UAAY,KAAa,CAAE,KAAM,SAAU,QAASA,EAAO,OAAA,EAC/D,CAAE,KAAM,SAAA,CACjB,+FC7CaG,EAAoB,GAAK,KAAO,KAE7C,SAASC,EAAgBC,EAAuB,CAC9C,MAAO,IAAIA,GAAS,KAAO,OAAO,QAAQ,CAAC,CAAC,KAC9C,CAEO,SAASC,EAAW,CAAE,KAAAd,GAAqB,aAChD,MAAMQ,EAASO,EAAYf,CAAI,EACzBgB,EAAWR,EAAO,iBAAiBS,EAAoBT,EAAO,MAAQ,KACtEU,EAAcF,IAAa,MAAQA,EAAS,MAAQL,EAEpDF,EAAWV,EAAYC,EAAMkB,CAAW,EACxC,CAACC,EAAmBC,CAAoB,EAAIC,EAAAA,SAAS,EAAK,EAC1DxB,EAAWI,EAAgBD,EAAMmB,CAAiB,EAElDG,EAAQhB,EAAsB,CAClC,gBAAiBK,EACjB,OAAQ,CACN,UAAWH,EAAO,UAClB,QAASA,EAAO,QAChB,SAAUQ,EAAW,CAAE,KAAMA,EAAS,MAAS,KAC/C,aAAcR,EAAO,iBAAiB,MAAQA,EAAO,MAAM,QAAU,KACrE,UAASL,EAAAK,EAAO,OAAP,YAAAL,EAAa,UAAW,IAAA,EAEnC,SAAU,CACR,UAAWM,EAAS,UACpB,QAASA,EAAS,QAClB,aAAcA,EAAS,iBAAiB,MAAQA,EAAS,MAAM,QAAU,KACzE,UAASc,EAAAd,EAAS,OAAT,YAAAc,EAAe,UAAW,IAAA,EAErC,SAAU,CACR,UAAWJ,EACX,UAAWtB,EAAS,UACpB,YAAW2B,EAAA3B,EAAS,OAAT,YAAA2B,EAAe,YAAa,KACvC,WAAUC,EAAA5B,EAAS,OAAT,YAAA4B,EAAe,WAAY,IAAA,CACvC,CACD,EAED,OAAQH,EAAM,KAAA,CACZ,IAAK,UACH,cACGI,EAAA,CACC,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,EACvB,EAEJ,IAAK,SACH,OAAOD,EAAAA,IAACE,EAAA,CAAS,KAAA7B,EAAY,QAASsB,EAAM,QAAS,EACvD,IAAK,YACH,cACGI,EAAA,CACC,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,cAAYhB,EAAgBU,EAAM,IAAI,EAAE,UAAA,EAC/D,EAEJ,IAAK,eACH,cAAQI,EAAA,CAAW,SAAA,CAAA,UAAQJ,EAAM,OAAA,EAAQ,EAC3C,IAAK,WACH,cACGQ,EAAA,CACC,SAAA,CAAAC,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAW,KAAO,SAAA,CAAApB,EAAgBU,EAAM,IAAI,EAAE,gBAAA,EAAc,EAC5EK,EAAAA,IAAC,MAAA,CAAI,UAAWK,EAAW,KACzB,SAAAL,EAAAA,IAACE,EAAA,CAAS,KAAA7B,EAAY,QAASsB,EAAM,OAAA,CAAS,CAAA,CAChD,CAAA,EACF,EAEJ,IAAK,YAAa,CAChB,MAAMW,EAAOjC,EAAK,MAAM,GAAG,EAAE,OAASA,EACtC,aACG0B,EAAA,CACC,SAAAK,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAW,SACzB,SAAA,CAAAD,OAAC,MAAA,CACE,SAAA,CAAAnB,EAAgBU,EAAM,IAAI,EAAE,8CAA4C,IACxEV,EAAgBU,EAAM,eAAe,EAAE,IAAA,EAC1C,EACCA,EAAM,YACLK,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EACjBN,EAAM,kBACP,IAAA,CAAE,KAAMA,EAAM,YAAa,SAAUA,EAAM,cAAgBW,EAC1D,SAAAN,MAACO,EAAA,CAAO,KAAK,KAAK,SAAA,WAAA,CAAS,EAC7B,EAEAP,EAAAA,IAACO,EAAA,CAAO,KAAK,KAAK,QAAS,IAAMd,EAAqB,EAAI,EAAG,SAAA,UAAA,CAE7D,CAAA,CAAA,CAEJ,CAAA,CACF,CAEJ,CACA,IAAK,QACH,cAAQM,EAAA,CAAW,SAAA,CAAA,UAAQJ,EAAM,OAAA,EAAQ,CAAA,CAE/C"}
@@ -1,2 +1,2 @@
1
- import{j as o}from"./vendor-react-CWo7Do4s.js";import{E as e,S as t}from"./index-DM5KSYdn.js";import{u as n}from"./use-file-download-BOCZENOG.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 a="_wrap_gy3e6_1",m="_video_gy3e6_10",i={wrap:a,video:m};function E({path:s}){const r=n(s);return r.isLoading?o.jsxs(e,{children:[o.jsx(t,{size:"sm"})," Loading video..."]}):r.isError?o.jsxs(e,{children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?o.jsx("div",{className:i.wrap,children:o.jsx("video",{className:i.video,src:r.data.objectUrl,controls:!0})}):null}export{E as VideoViewer};
2
- //# sourceMappingURL=VideoViewer-rhO-urrJ.js.map
1
+ import{j as o}from"./vendor-react-CWo7Do4s.js";import{E as e,S as t}from"./index-DonKPQN3.js";import{u as n}from"./use-file-download-C2Lrmbfe.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 a="_wrap_gy3e6_1",m="_video_gy3e6_10",i={wrap:a,video:m};function E({path:s}){const r=n(s);return r.isLoading?o.jsxs(e,{children:[o.jsx(t,{size:"sm"})," Loading video..."]}):r.isError?o.jsxs(e,{children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?o.jsx("div",{className:i.wrap,children:o.jsx("video",{className:i.video,src:r.data.objectUrl,controls:!0})}):null}export{E as VideoViewer};
2
+ //# sourceMappingURL=VideoViewer-DeIrV1Ql.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"VideoViewer-rhO-urrJ.js","sources":["../../../src/features/files/VideoViewer.tsx"],"sourcesContent":["/**\n * VideoViewer — renders a binary video file via the file-transfer binary download\n * (`useFileDownload`) into a native `<video>` element (POC_TO_APP_PLAN_UI.md §4.5 follow-up:\n * modular file preview).\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 \"./VideoViewer.module.css\";\n\nexport function VideoViewer({ path }: ViewerProps) {\n const query = useFileDownload(path);\n\n if (query.isLoading) {\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading video...\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 {/* eslint-disable-next-line jsx-a11y/media-has-caption -- arbitrary local file, no caption track available */}\n <video className={styles.video} src={query.data.objectUrl} controls />\n </div>\n );\n}\n"],"names":["VideoViewer","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,WAGR,MAAA,CAAI,UAAWK,EAAO,KAErB,eAAC,QAAA,CAAM,UAAWA,EAAO,MAAO,IAAKL,EAAM,KAAK,UAAW,SAAQ,GAAC,EACtE,EANsB,IAQ1B"}
1
+ {"version":3,"file":"VideoViewer-DeIrV1Ql.js","sources":["../../../src/features/files/VideoViewer.tsx"],"sourcesContent":["/**\n * VideoViewer — renders a binary video file via the file-transfer binary download\n * (`useFileDownload`) into a native `<video>` element (POC_TO_APP_PLAN_UI.md §4.5 follow-up:\n * modular file preview).\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 \"./VideoViewer.module.css\";\n\nexport function VideoViewer({ path }: ViewerProps) {\n const query = useFileDownload(path);\n\n if (query.isLoading) {\n return (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading video...\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 {/* eslint-disable-next-line jsx-a11y/media-has-caption -- arbitrary local file, no caption track available */}\n <video className={styles.video} src={query.data.objectUrl} controls />\n </div>\n );\n}\n"],"names":["VideoViewer","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,WAGR,MAAA,CAAI,UAAWK,EAAO,KAErB,eAAC,QAAA,CAAM,UAAWA,EAAO,MAAO,IAAKL,EAAM,KAAK,UAAW,SAAQ,GAAC,EACtE,EANsB,IAQ1B"}