@av-pi-studio/web-client 0.0.73 → 0.0.74

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 (32) hide show
  1. package/dist/web/assets/{BinaryFallbackViewer-C7jh_fjO.js → BinaryFallbackViewer-B1PBcT3N.js} +2 -2
  2. package/dist/web/assets/{BinaryFallbackViewer-C7jh_fjO.js.map → BinaryFallbackViewer-B1PBcT3N.js.map} +1 -1
  3. package/dist/web/assets/{ChatPanel-DlHBIYiO.js → ChatPanel-DuiC5ipg.js} +2 -2
  4. package/dist/web/assets/{ChatPanel-DlHBIYiO.js.map → ChatPanel-DuiC5ipg.js.map} +1 -1
  5. package/dist/web/assets/{CodeView-DejUdSHl.js → CodeView-_wBYU-zK.js} +2 -2
  6. package/dist/web/assets/{CodeView-DejUdSHl.js.map → CodeView-_wBYU-zK.js.map} +1 -1
  7. package/dist/web/assets/{FilePanel-DtAGc7Lw.js → FilePanel-CqiLosrb.js} +2 -2
  8. package/dist/web/assets/{FilePanel-DtAGc7Lw.js.map → FilePanel-CqiLosrb.js.map} +1 -1
  9. package/dist/web/assets/{ImageViewer-YLGH2uig.js → ImageViewer-WToUsK2Y.js} +2 -2
  10. package/dist/web/assets/{ImageViewer-YLGH2uig.js.map → ImageViewer-WToUsK2Y.js.map} +1 -1
  11. package/dist/web/assets/{MarkdownFileViewer-C_4HZa34.js → MarkdownFileViewer-Do69vE6s.js} +2 -2
  12. package/dist/web/assets/{MarkdownFileViewer-C_4HZa34.js.map → MarkdownFileViewer-Do69vE6s.js.map} +1 -1
  13. package/dist/web/assets/{MoleculeViewerPanel-CNaBH6ND.js → MoleculeViewerPanel-CKaIka-k.js} +2 -2
  14. package/dist/web/assets/{MoleculeViewerPanel-CNaBH6ND.js.map → MoleculeViewerPanel-CKaIka-k.js.map} +1 -1
  15. package/dist/web/assets/{TerminalPanel-aflKubq7.js → TerminalPanel-B7ujFxCG.js} +2 -2
  16. package/dist/web/assets/{TerminalPanel-aflKubq7.js.map → TerminalPanel-B7ujFxCG.js.map} +1 -1
  17. package/dist/web/assets/{TextViewer-DBXXTsTN.js → TextViewer-CJGwYjCh.js} +2 -2
  18. package/dist/web/assets/{TextViewer-DBXXTsTN.js.map → TextViewer-CJGwYjCh.js.map} +1 -1
  19. package/dist/web/assets/{VideoViewer-XGCQzxEz.js → VideoViewer-CYXPkYLV.js} +2 -2
  20. package/dist/web/assets/{VideoViewer-XGCQzxEz.js.map → VideoViewer-CYXPkYLV.js.map} +1 -1
  21. package/dist/web/assets/index-Cz7AgOgN.js +18 -0
  22. package/dist/web/assets/index-Cz7AgOgN.js.map +1 -0
  23. package/dist/web/assets/{markdown-DGGFhmLR.js → markdown-q0cxuHgm.js} +2 -2
  24. package/dist/web/assets/{markdown-DGGFhmLR.js.map → markdown-q0cxuHgm.js.map} +1 -1
  25. package/dist/web/assets/{use-file-download-RWF-ZBv9.js → use-file-download-BCG_kqpD.js} +2 -2
  26. package/dist/web/assets/{use-file-download-RWF-ZBv9.js.map → use-file-download-BCG_kqpD.js.map} +1 -1
  27. package/dist/web/assets/{use-file-watch-B-wXmE8p.js → use-file-watch-CZgwolg7.js} +2 -2
  28. package/dist/web/assets/{use-file-watch-B-wXmE8p.js.map → use-file-watch-CZgwolg7.js.map} +1 -1
  29. package/dist/web/index.html +1 -1
  30. package/package.json +3 -3
  31. package/dist/web/assets/index-CVbpa3Qk.js +0 -18
  32. package/dist/web/assets/index-CVbpa3Qk.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as f,j as s}from"./vendor-react-Uv3j2inr.js";import{C as v}from"./vendor-BLc7PaxD.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-CVbpa3Qk.js";import{u as S,c as k}from"./vendor-query-NxjRJKdp.js";import{u as q}from"./use-file-watch-B-wXmE8p.js";import{D as z}from"./DiffView-dzXC51QP.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.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-DtAGc7Lw.js.map
1
+ import{r as f,j as s}from"./vendor-react-Uv3j2inr.js";import{C as v}from"./vendor-BLc7PaxD.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-Cz7AgOgN.js";import{u as S,c as k}from"./vendor-query-NxjRJKdp.js";import{u as q}from"./use-file-watch-CZgwolg7.js";import{D as z}from"./DiffView-dzXC51QP.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.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-CqiLosrb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FilePanel-DtAGc7Lw.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-CqiLosrb.js","sources":["../../../src/hooks/use-file-diff.ts","../../../src/hooks/use-file-live-refresh.ts","../../../src/features/files/FilePanel.tsx"],"sourcesContent":["/**\n * `use-file-diff` — TanStack Query wrapper over `file_diff_request` (POC `loadDiffContent`,\n * POC_TO_APP_PLAN_UI.md §4.5 / §3 `hooks/use-file-diff.ts`).\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface FileDiffResult {\n patch: string;\n}\n\ninterface FileDiffRpcResponse {\n patch?: string;\n}\n\nexport function useFileDiff(path: string, cwd: string, staged: boolean, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.fileDiff(path, cwd, staged),\n queryFn: async (): Promise<FileDiffResult> => {\n if (!client) throw new Error(\"not connected\");\n const response = await client.connection.request<FileDiffRpcResponse>(\"file_diff_request\", {\n path,\n cwd,\n staged,\n });\n return { patch: response.patch ?? \"\" };\n },\n enabled: Boolean(client) && Boolean(path) && Boolean(cwd) && enabled,\n });\n}\n","/**\n * `use-file-live-refresh` — watches a file tab's OWN file (via `useFileWatch`/`watchFile`) and\n * invalidates that path's content queries on every `file_changed` push, so a `kind:\"file\"` or\n * `kind:\"diff\"` tab refetches from disk within ~1s of an external change — an editor, a shell\n * command, a build step, or `git` run in a terminal — not just a Pi tool call completing\n * (`lib/connection/files-changed.ts`'s existing debounced post-tool-call invalidation, which stays\n * as the belt-and-braces path for files nobody has open).\n *\n * `LIVE_REFRESH_KINDS` is the scope gate: only `\"text\"`, `\"markdown\"`, and `\"image\"` tabs watch.\n * `\"video\"` is excluded because a refetch mints a new object URL, which restarts playback from\n * zero mid-watch; `\"binary\"` fetches nothing at all until the user clicks its Download button, so\n * there is nothing to refresh. Exported so the gate itself is unit-testable — a new `ViewerKind`\n * must make an explicit choice here, not inherit one.\n */\n\nimport { useEffect } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { resolveWorkspacePath } from \"@pi-studio-ui/lib/paths.js\";\nimport type { ViewerKind } from \"@pi-studio-ui/features/files/viewer-registry.js\";\nimport { useFileWatch } from \"./use-file-watch.js\";\n\nexport const LIVE_REFRESH_KINDS: ReadonlySet<ViewerKind> = new Set([\"text\", \"markdown\", \"image\"]);\n\n/**\n * Invalidates `path`'s `fileRead`/`fileDownload`/`fileDiffByPath` queries on every `file_changed`\n * push for the WATCHED (resolved absolute) target — these differ for a diff tab, which is why\n * both `path` and `cwd` are taken separately. All three keys are invalidated unconditionally: a\n * watched tab can be showing any of them (`fileRead` → `TextViewer` tier 1 + `MarkdownFileViewer`;\n * `fileDownload` → `ImageViewer` and `TextViewer`'s tier-2 streamed source; `fileDiffByPath` →\n * `DiffView`, reachable from both tab kinds through `FilePanel`'s File/Diff toggle). Invalidating a\n * key with no live query is a no-op, so only *whether to watch* branches on `viewerKind`, never\n * the invalidation list.\n *\n * No client-side debounce: the daemon already coalesces at `FILE_WATCH_COALESCE_MS` (150ms). A\n * deleted file pushes too — the refetch then fails and the viewer shows its existing error state,\n * which is the correct observable outcome; no special-casing.\n */\nexport function useFileLiveRefresh(path: string, cwd: string, viewerKind: ViewerKind): void {\n const queryClient = useQueryClient();\n // A `null` target subscribes nothing (`useFileWatch`'s documented no-op), so an excluded kind\n // costs zero daemon watches rather than watching and then discarding the push.\n const target = LIVE_REFRESH_KINDS.has(viewerKind) ? resolveWorkspacePath(path, cwd) : null;\n const { changedAt } = useFileWatch(target);\n\n useEffect(() => {\n if (changedAt === null) return;\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileRead(path) });\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileDownload(path) });\n void queryClient.invalidateQueries({ queryKey: rpcKeys.fileDiffByPath(path) });\n }, [changedAt, path, queryClient]);\n}\n","/**\n * FilePanel — the file/diff view toggle panel (POC `buildFilePanel`/`buildDiffPanel` +\n * `loadFileContent`/`loadDiffContent`, chat.html ~line 800-900, POC_TO_APP_PLAN_UI.md §4.5).\n * `panel-registry.ts` mounts this component for BOTH `kind:\"file\"` and `kind:\"diff\"` tabs — a\n * `kind:\"file\"` tab defaults to the File view, a `kind:\"diff\"` tab defaults to the Diff view.\n * Both views stay toggleable within the same panel via local `viewMode` state (not tab-store\n * state), matching the POC's per-panel `showFile`/`showDiff` toggle.\n *\n * The File view dispatches through the modular viewer registry (`viewer-registry.ts`):\n * `detectViewerKind` picks a viewer from the path's extension, and `VIEWER_BY_KIND` resolves the\n * (lazily-loaded) component — text/markdown/image/video today, more by adding one registry entry.\n *\n * Live updates come from the daemon's filesystem watcher via `useFileLiveRefresh` (not the\n * debounced post-tool-call invalidation in `lib/connection/files-changed.ts`, which stays as the\n * fallback for files nobody has open): only `text`/`markdown`/`image` kinds are watched — a video\n * refetch would restart playback from zero, and the binary fallback fetches nothing eagerly, so\n * there is nothing to refresh. Molecule tabs are deliberately NOT covered here — they mount\n * `MoleculeViewerPanel` elsewhere, whose `MoleculeViewer` keeps its own `shouldApplyRefresh`\n * unsaved-edits gate that this generic invalidation would bypass.\n */\n\nimport { Suspense, useState } from \"react\";\nimport { clsx } from \"clsx\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useFileDiff } from \"@pi-studio-ui/hooks/use-file-diff.js\";\nimport { useFileLiveRefresh } from \"@pi-studio-ui/hooks/use-file-live-refresh.js\";\nimport type { Tab, FileTabData, DiffTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { DiffView } from \"./DiffView.js\";\nimport { detectViewerKind, VIEWER_BY_KIND } from \"./viewer-registry.js\";\nimport styles from \"./FilePanel.module.css\";\n\ntype ViewMode = \"file\" | \"diff\";\n\nfunction hasPath(data: Tab[\"data\"]): data is FileTabData | DiffTabData {\n return \"path\" in data;\n}\n\nfunction isDiffTabData(data: FileTabData | DiffTabData): data is DiffTabData {\n return \"staged\" in data;\n}\n\nexport interface FilePanelProps {\n tab: Tab;\n}\n\nexport function FilePanel({ tab }: FilePanelProps) {\n const isDiffTab = tab.kind === \"diff\";\n const data = tab.data;\n const path = hasPath(data) ? data.path : \"\";\n const staged = hasPath(data) && isDiffTabData(data) ? data.staged : false;\n\n const [viewMode, setViewMode] = useState<ViewMode>(isDiffTab ? \"diff\" : \"file\");\n const cwd = tab.workspaceCwd || \"~\";\n const viewerKind = detectViewerKind(path);\n useFileLiveRefresh(path, cwd, viewerKind);\n\n const diffQuery = useFileDiff(path, cwd, staged, viewMode === \"diff\");\n\n const Viewer = VIEWER_BY_KIND[viewerKind];\n\n return (\n <Panel>\n <div className={styles.header}>\n <span className={styles.path}>{path}</span>\n <span className={styles.size}>\n {viewMode === \"diff\" ? (staged ? \"staged\" : \"unstaged\") : null}\n </span>\n <div className={styles.viewToggle}>\n <Button\n size=\"xs\"\n variant={viewMode === \"file\" ? \"default\" : \"ghost\"}\n onClick={() => setViewMode(\"file\")}\n >\n File\n </Button>\n <Button\n size=\"xs\"\n variant={viewMode === \"diff\" ? \"default\" : \"ghost\"}\n onClick={() => setViewMode(\"diff\")}\n >\n Diff\n </Button>\n </div>\n </div>\n <div className={clsx(styles.body, \"file-body\")}>\n {viewMode === \"file\" ? (\n <Suspense\n fallback={\n <EmptyState>\n <Spinner size=\"sm\" /> Loading...\n </EmptyState>\n }\n >\n <Viewer path={path} />\n </Suspense>\n ) : diffQuery.isLoading ? (\n <EmptyState>\n <Spinner size=\"sm\" /> Loading diff...\n </EmptyState>\n ) : diffQuery.isError ? (\n <EmptyState>\n Error: {diffQuery.error instanceof Error ? diffQuery.error.message : \"unknown error\"}\n </EmptyState>\n ) : diffQuery.data?.patch ? (\n <DiffView patch={diffQuery.data.patch} />\n ) : (\n <EmptyState style={{ padding: \"var(--pi-spacing-30)\" }}>\n No changes (file matches HEAD)\n </EmptyState>\n )}\n </div>\n </Panel>\n );\n}\n"],"names":["useFileDiff","path","cwd","staged","enabled","client","useConnectionStore","s","useQuery","rpcKeys","LIVE_REFRESH_KINDS","useFileLiveRefresh","viewerKind","queryClient","useQueryClient","target","resolveWorkspacePath","changedAt","useFileWatch","useEffect","hasPath","data","isDiffTabData","FilePanel","tab","isDiffTab","viewMode","setViewMode","useState","detectViewerKind","diffQuery","Viewer","VIEWER_BY_KIND","Panel","jsxs","styles","jsx","Button","clsx","Suspense","EmptyState","Spinner","_a","DiffView"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{j as o}from"./vendor-react-Uv3j2inr.js";import{E as e,S as t}from"./index-CVbpa3Qk.js";import{u as a}from"./use-file-download-RWF-ZBv9.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-BLc7PaxD.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.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-YLGH2uig.js.map
1
+ import{j as o}from"./vendor-react-Uv3j2inr.js";import{E as e,S as t}from"./index-Cz7AgOgN.js";import{u as a}from"./use-file-download-BCG_kqpD.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-BLc7PaxD.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.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-WToUsK2Y.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImageViewer-YLGH2uig.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-WToUsK2Y.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-Uv3j2inr.js";import{E as n,S as c,B as a}from"./index-CVbpa3Qk.js";import{u as l,C as m}from"./CodeView-DejUdSHl.js";import{M as p}from"./markdown-DGGFhmLR.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-BLc7PaxD.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.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-C_4HZa34.js.map
1
+ import{r as d,j as r}from"./vendor-react-Uv3j2inr.js";import{E as n,S as c,B as a}from"./index-Cz7AgOgN.js";import{u as l,C as m}from"./CodeView-_wBYU-zK.js";import{M as p}from"./markdown-q0cxuHgm.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-BLc7PaxD.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.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-Do69vE6s.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownFileViewer-C_4HZa34.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-Do69vE6s.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 c,j as t}from"./vendor-react-Uv3j2inr.js";import{a as I,m as W,P as h,E as N,S as k,l as q,o as C,p as U,q as X,C as Y,v as z,h as D}from"./index-CVbpa3Qk.js";import{y as G}from"./vendor-molviewer-B2jrWzvr.js";import{S as v}from"./StatusBadge-DggMFuW_.js";import{u as H}from"./use-file-download-RWF-ZBv9.js";import{u as K}from"./use-file-watch-B-wXmE8p.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-BLc7PaxD.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.js";function J(e,r){return!e||!r?null:{url:r,name:e.split("/").pop()||e}}function Q({changedAt:e,lastAppliedAt:r,modified:n}){return!(e===null||n||e===r)}const Z={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 ee(e,r,n){const a=await e.connection.request("file_write_request",{path:r,content:n});if(!a.ok){const l=a.error??"";throw new Error(Z[l]??l??"Failed to save")}}const re="mol2";function oe(e){const{sourcePath:r,monomers:n,attempt:a}=e,l=a>0?`_${a+1}`:"";return`${te(r)}_polymer_${n}${l}.${re}`}function te(e){return(e.split("/").pop()??"").replace(/\.[^.]+$/,"").trim()||"polymer"}const se={"--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)"},ne="_wrap_tixu9_1",ae="_molViewer_tixu9_4",le="_badges_tixu9_8",g={wrap:ne,molViewer:ae,badges:le},ie=50;async function ce(e){const{client:r,upload:n,sourcePath:a,monomers:l,text:f}=e,p=U(a);let s=null,i="";for(let u=0;u<ie&&s===null;u+=1){i=oe({sourcePath:a,monomers:l,attempt:u});try{s=await X(r,p,i,"file")}catch(m){if(m instanceof Y&&m.code==="exists")continue;throw m}}if(s===null)throw new Error(`No free name for ${i} — too many builds in this folder.`);try{await n(p,new File([f],i,{type:"chemical/x-mol2"}))}catch(u){throw await z(r,s).catch(()=>{}),u}return s}function ue({path:e,workspaceCwd:r,tabId:n,isActive:a,onModifiedChange:l}){var F;const f=I(o=>o.client),{upload:p}=W(),s=H(e??"",!!e),{changedAt:i}=K(e),u=c.useRef(null),m=c.useRef(!1),[w,_]=c.useState(null),[E,A]=c.useState(!1),[S,x]=c.useState(null),[j,y]=c.useState(null),[M,P]=c.useState(null),b=c.useRef(null),R=((F=s.data)==null?void 0:F.objectUrl)??null,$=c.useMemo(()=>J(e,R),[e,R]);c.useEffect(()=>{Q({changedAt:i,lastAppliedAt:b.current,modified:E})&&(b.current=i,s.refetch())},[i,E,s.refetch]);const L=i!==null&&i!==b.current&&E;async function V(o){if(e){if(!f){x("Not connected — cannot save.");return}x(null);try{await ee(f,e,o.text()),o.saved()}catch(d){x(d instanceof Error?d.message:"Failed to save")}}}async function B(o){if(e){if(!f){y("Not connected — cannot save the polymer.");return}y(null),P(null);try{const d=await ce({client:f,upload:p,sourcePath:e,monomers:o.monomers,text:o.text()});o.report.clashes>0&&P(`${o.report.clashes} close contacts — relax the geometry before simulating`);const O=q.getState().paneOfTab(r,n);C(d,r,O??void 0)}catch(d){y(d instanceof Error?d.message:"Failed to save the polymer")}}}const T=(s.isError?s.error instanceof Error?s.error.message:"download failed":null)??(w==null?void 0:w.message)??null;return e&&s.isPending?t.jsx(h,{children:t.jsxs(N,{children:[t.jsx(k,{size:"sm"})," Loading..."]})}):T?t.jsx(h,{children:t.jsxs(N,{children:["Error: ",T]})}):t.jsxs(h,{className:g.wrap,"data-molecule-active":a?"true":"false",children:[t.jsxs("div",{className:g.badges,children:[L&&t.jsx(v,{label:"File changed on disk",variant:"muted"}),S&&t.jsx(v,{label:`Save failed: ${S}`,variant:"error"}),j&&t.jsx(v,{label:`Polymer build: ${j}`,variant:"error"}),M&&t.jsx(v,{label:M,variant:"warning"})]}),t.jsx(G,{ref:u,className:g.molViewer,theme:se,source:$,sourceMode:m.current?"update":"replace",onLoad:()=>{m.current=!0,_(null)},onLoadError:o=>_(o.error),onModifiedChange:o=>{A(o),l==null||l(o)},onSave:e?V:void 0,onPolymerBuild:e?B:void 0})]})}function je({tab:e}){const{path:r}=e.data,n=D(e.id);return t.jsx(h,{children:t.jsx(ue,{path:r,workspaceCwd:e.workspaceCwd,tabId:e.id,isActive:n})})}export{je as MoleculeViewerPanel};
2
- //# sourceMappingURL=MoleculeViewerPanel-CNaBH6ND.js.map
1
+ import{r as c,j as t}from"./vendor-react-Uv3j2inr.js";import{a as I,m as W,P as h,E as N,S as k,l as q,o as C,p as U,q as X,C as Y,v as z,h as D}from"./index-Cz7AgOgN.js";import{y as G}from"./vendor-molviewer-B2jrWzvr.js";import{S as v}from"./StatusBadge-DggMFuW_.js";import{u as H}from"./use-file-download-BCG_kqpD.js";import{u as K}from"./use-file-watch-CZgwolg7.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-BLc7PaxD.js";import"./vendor-markdown-DtEbK_xy.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.js";function J(e,r){return!e||!r?null:{url:r,name:e.split("/").pop()||e}}function Q({changedAt:e,lastAppliedAt:r,modified:n}){return!(e===null||n||e===r)}const Z={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 ee(e,r,n){const a=await e.connection.request("file_write_request",{path:r,content:n});if(!a.ok){const l=a.error??"";throw new Error(Z[l]??l??"Failed to save")}}const re="mol2";function oe(e){const{sourcePath:r,monomers:n,attempt:a}=e,l=a>0?`_${a+1}`:"";return`${te(r)}_polymer_${n}${l}.${re}`}function te(e){return(e.split("/").pop()??"").replace(/\.[^.]+$/,"").trim()||"polymer"}const se={"--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)"},ne="_wrap_tixu9_1",ae="_molViewer_tixu9_4",le="_badges_tixu9_8",g={wrap:ne,molViewer:ae,badges:le},ie=50;async function ce(e){const{client:r,upload:n,sourcePath:a,monomers:l,text:f}=e,p=U(a);let s=null,i="";for(let u=0;u<ie&&s===null;u+=1){i=oe({sourcePath:a,monomers:l,attempt:u});try{s=await X(r,p,i,"file")}catch(m){if(m instanceof Y&&m.code==="exists")continue;throw m}}if(s===null)throw new Error(`No free name for ${i} — too many builds in this folder.`);try{await n(p,new File([f],i,{type:"chemical/x-mol2"}))}catch(u){throw await z(r,s).catch(()=>{}),u}return s}function ue({path:e,workspaceCwd:r,tabId:n,isActive:a,onModifiedChange:l}){var F;const f=I(o=>o.client),{upload:p}=W(),s=H(e??"",!!e),{changedAt:i}=K(e),u=c.useRef(null),m=c.useRef(!1),[w,_]=c.useState(null),[E,A]=c.useState(!1),[S,x]=c.useState(null),[j,y]=c.useState(null),[M,P]=c.useState(null),b=c.useRef(null),R=((F=s.data)==null?void 0:F.objectUrl)??null,$=c.useMemo(()=>J(e,R),[e,R]);c.useEffect(()=>{Q({changedAt:i,lastAppliedAt:b.current,modified:E})&&(b.current=i,s.refetch())},[i,E,s.refetch]);const L=i!==null&&i!==b.current&&E;async function V(o){if(e){if(!f){x("Not connected — cannot save.");return}x(null);try{await ee(f,e,o.text()),o.saved()}catch(d){x(d instanceof Error?d.message:"Failed to save")}}}async function B(o){if(e){if(!f){y("Not connected — cannot save the polymer.");return}y(null),P(null);try{const d=await ce({client:f,upload:p,sourcePath:e,monomers:o.monomers,text:o.text()});o.report.clashes>0&&P(`${o.report.clashes} close contacts — relax the geometry before simulating`);const O=q.getState().paneOfTab(r,n);C(d,r,O??void 0)}catch(d){y(d instanceof Error?d.message:"Failed to save the polymer")}}}const T=(s.isError?s.error instanceof Error?s.error.message:"download failed":null)??(w==null?void 0:w.message)??null;return e&&s.isPending?t.jsx(h,{children:t.jsxs(N,{children:[t.jsx(k,{size:"sm"})," Loading..."]})}):T?t.jsx(h,{children:t.jsxs(N,{children:["Error: ",T]})}):t.jsxs(h,{className:g.wrap,"data-molecule-active":a?"true":"false",children:[t.jsxs("div",{className:g.badges,children:[L&&t.jsx(v,{label:"File changed on disk",variant:"muted"}),S&&t.jsx(v,{label:`Save failed: ${S}`,variant:"error"}),j&&t.jsx(v,{label:`Polymer build: ${j}`,variant:"error"}),M&&t.jsx(v,{label:M,variant:"warning"})]}),t.jsx(G,{ref:u,className:g.molViewer,theme:se,source:$,sourceMode:m.current?"update":"replace",onLoad:()=>{m.current=!0,_(null)},onLoadError:o=>_(o.error),onModifiedChange:o=>{A(o),l==null||l(o)},onSave:e?V:void 0,onPolymerBuild:e?B:void 0})]})}function je({tab:e}){const{path:r}=e.data,n=D(e.id);return t.jsx(h,{children:t.jsx(ue,{path:r,workspaceCwd:e.workspaceCwd,tabId:e.id,isActive:n})})}export{je as MoleculeViewerPanel};
2
+ //# sourceMappingURL=MoleculeViewerPanel-CKaIka-k.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MoleculeViewerPanel-CNaBH6ND.js","sources":["../../../src/features/files/molecule-source.ts","../../../src/features/files/molecule-reload.ts","../../../src/features/files/write-file.ts","../../../src/features/files/polymer-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 * Name derivation for a polymer built inside the molecule viewer (`@molviewer/core`'s\n * `onPolymerBuild`, 0.4.4+).\n *\n * Naming the file is deliberately the HOST's job. `PolymerBuildEvent` reports the monomer's name\n * and the build settings but never a target, because only the host can see the directory and so\n * only the host knows which names are already taken.\n *\n * Kept pure and separate from `MoleculeViewer.tsx` so it is testable under the root vitest runner,\n * which discovers `.test.ts` under a node environment only — same split as its siblings\n * `molecule-source.ts` and `molecule-reload.ts`.\n *\n * Building does not change what is on screen, so pressing Build repeatedly is normal and every\n * press must produce its own file. `polymerFileName` is therefore an ATTEMPT-indexed sequence,\n * walked by the caller against the create-exclusive `file_create_request` until one lands — the\n * filesystem arbitrates the collision, not a directory listing that could be stale.\n */\n\n/** Polymers always leave molviewer as MOL2: the builder CONSTRUCTS the junction bonds and marks\n * them authoritative, so a format that cannot carry a bond list would lose them outright. */\nconst POLYMER_EXTENSION = \"mol2\";\n\n/**\n * Candidate file name for attempt `attempt` (0-based) of a build off the monomer at `sourcePath`.\n *\n * Attempt 0 is `<monomer-stem>_polymer_<monomers>.mol2`; every later attempt appends its own\n * ordinal — `_2`, `_3`, … — so the trailing number reads as \"the Nth file with these settings\"\n * and the first one needs no suffix at all.\n *\n * `monomers` is the chain length in monomer units (`PolymerBuildEvent.monomers`), not the physical\n * length in Å that `report` carries.\n */\nexport function polymerFileName(args: {\n sourcePath: string;\n monomers: number;\n attempt: number;\n}): string {\n const { sourcePath, monomers, attempt } = args;\n const suffix = attempt > 0 ? `_${attempt + 1}` : \"\";\n return `${polymerStem(sourcePath)}_polymer_${monomers}${suffix}.${POLYMER_EXTENSION}`;\n}\n\n/**\n * Base name of the monomer with its directory and extension stripped.\n *\n * Taken from the tab's own absolute path rather than `PolymerBuildEvent.sourceFileName` for the\n * same reason the save path ignores `SaveEvent.fileName`: the event carries molviewer's load-time\n * basename, while the path prop is the authoritative location the tab was opened from. It also\n * makes the event's `sourceFileName: null` case (a monomer drawn in the viewer) unreachable here —\n * that tab has no path, so it never gets a build handler in the first place.\n *\n * Falls back to `polymer` for a name that is entirely extension (`.pdb`), which would otherwise\n * produce a leading-underscore stem.\n */\nfunction polymerStem(sourcePath: string): string {\n const base = sourcePath.split(\"/\").pop() ?? \"\";\n return base.replace(/\\.[^.]+$/, \"\").trim() || \"polymer\";\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.3+): 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 * Polymer build (`onPolymerBuild`, `@molviewer/core` 0.4.4+): file-backed tabs only, like `onSave`\n * — but omitting it does NOT hide the Build button the way omitting `onSave` hides Save. With no\n * handler molviewer downloads the .mol2 itself, which is the right answer for a `path`-null\n * (\"+\"-menu) tab: there is no directory to write beside.\n *\n * Building deliberately does not change what is on screen — the monomer stays loaded and the\n * polymer is a NEW structure — so this writes it beside the monomer and opens it in its own\n * molecule tab, in the SAME pane this viewer lives in. See `writePolymer` below for the naming\n * and rollback contract.\n */\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n MolViewer,\n type MolViewerHandle,\n type PolymerBuildEvent,\n type SaveEvent,\n} from \"@molviewer/core\";\nimport \"@molviewer/core/style.css\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\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 { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { dirOf } from \"@pi-studio-ui/lib/paths.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { useFileWatch } from \"@pi-studio-ui/hooks/use-file-watch.js\";\nimport { useFileTransfer } from \"@pi-studio-ui/hooks/use-file-transfer.js\";\nimport { moleculeSource } from \"./molecule-source.js\";\nimport { shouldApplyRefresh } from \"./molecule-reload.js\";\nimport { writeFile } from \"./write-file.js\";\nimport { createEntry, CreateEntryError } from \"./create-entry.js\";\nimport { deleteEntry } from \"./delete-entry.js\";\nimport { openMoleculeTab } from \"./open-file-tab.js\";\nimport { polymerFileName } from \"./polymer-file.js\";\nimport { MOLVIEWER_THEME } from \"./molecule-theme.js\";\nimport styles from \"./MoleculeViewer.module.css\";\n\n/** How many `<stem>_polymer_<n>[_k].mol2` candidates to claim before giving up on a free name. A\n * ceiling only: every attempt past the first means that many builds already landed in this folder\n * with these settings, and 50 of them is a user problem rather than a naming one. */\nconst MAX_POLYMER_NAME_ATTEMPTS = 50;\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 /** Workspace this viewer's tab belongs to — needed to open the polymer's tab in the same\n * workspace. Same normalized cwd the tab was minted with, never re-derived from the active\n * session. */\n workspaceCwd: string;\n /** This viewer's own tab id, used to resolve which pane a built polymer's tab should join.\n * Without it the polymer would land in the FOCUSED pane, which is not necessarily the one the\n * monomer is being viewed in. */\n tabId: string;\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\n/**\n * Claim a free name next to `sourcePath` and fill it with `text`; returns the path written.\n *\n * The name is claimed with `file_create_request` rather than chosen from a directory listing:\n * that RPC opens `wx` (create-exclusive), so the filesystem itself arbitrates the collision and a\n * stale listing can never cause an overwrite. Each `exists` just advances to the next candidate.\n *\n * Content then goes through the binary upload stream, not `file_write_request`, because the latter\n * caps at `MAX_INLINE_FILE_READ_BYTES` (5 MiB) — reachable for a long chain of a large monomer,\n * and a build silently failing at some atom count is not a behaviour worth shipping. The upload\n * opens the target `\"w\"`, so it fills the empty file the claim just created.\n *\n * A failed upload rolls the claim back: without it every failure would litter the folder with a\n * 0-byte .mol2 that also burns its candidate name for good. The rollback is best-effort — the\n * upload error is what the user needs to see, and a delete that also fails must not mask it.\n */\nasync function writePolymer(args: {\n client: PiStudioClient;\n upload: (dir: string, file: File) => Promise<void>;\n sourcePath: string;\n monomers: number;\n text: string;\n}): Promise<string> {\n const { client, upload, sourcePath, monomers, text } = args;\n const dir = dirOf(sourcePath);\n\n let claimed: string | null = null;\n let name = \"\";\n for (let attempt = 0; attempt < MAX_POLYMER_NAME_ATTEMPTS && claimed === null; attempt += 1) {\n name = polymerFileName({ sourcePath, monomers, attempt });\n try {\n claimed = await createEntry(client, dir, name, \"file\");\n } catch (err) {\n if (err instanceof CreateEntryError && err.code === \"exists\") continue;\n throw err;\n }\n }\n if (claimed === null)\n throw new Error(`No free name for ${name} — too many builds in this folder.`);\n\n try {\n await upload(dir, new File([text], name, { type: \"chemical/x-mol2\" }));\n } catch (err) {\n await deleteEntry(client, claimed).catch(() => {});\n throw err;\n }\n return claimed;\n}\n\nexport function MoleculeViewer({\n path,\n workspaceCwd,\n tabId,\n isActive,\n onModifiedChange,\n}: MoleculeViewerProps) {\n const client = useConnectionStore((s) => s.client);\n const { upload } = useFileTransfer();\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 const [buildError, setBuildError] = useState<string | null>(null);\n const [buildWarning, setBuildWarning] = 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 /**\n * A polymer was built. Nothing on screen changes — the monomer stays loaded — so the whole job\n * here is getting the new structure onto disk beside it and into its own tab.\n *\n * `e.text()` is a thunk and is called exactly once, up front: serialising a long chain costs\n * real time, and calling it after the name is claimed would leave an empty file behind if\n * serialisation threw.\n *\n * The new tab joins THIS viewer's pane (`paneOfTab`) rather than the focused one, so a build\n * started in a background pane doesn't fling its result across the workspace. `openMoleculeTab`\n * treats an unknown pane id as \"not supplied\" and falls back to the focused pane, so a null from\n * `paneOfTab` needs no special case.\n */\n async function handlePolymerBuild(e: PolymerBuildEvent) {\n if (!path) return;\n if (!client) {\n setBuildError(\"Not connected — cannot save the polymer.\");\n return;\n }\n setBuildError(null);\n setBuildWarning(null);\n try {\n const created = await writePolymer({\n client,\n upload,\n sourcePath: path,\n monomers: e.monomers,\n text: e.text(),\n });\n // Rigid placement is never minimised, so overlapping atoms are a real result the user has to\n // know about before feeding this into anything. Surfaced after the write, not instead of it:\n // the file is still valid and still theirs.\n if (e.report.clashes > 0) {\n setBuildWarning(\n `${e.report.clashes} close contacts — relax the geometry before simulating`,\n );\n }\n const pane = useLayoutStore.getState().paneOfTab(workspaceCwd, tabId);\n openMoleculeTab(created, workspaceCwd, pane ?? undefined);\n } catch (err) {\n setBuildError(err instanceof Error ? err.message : \"Failed to save the polymer\");\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 {buildError && <StatusBadge label={`Polymer build: ${buildError}`} variant=\"error\" />}\n {buildWarning && <StatusBadge label={buildWarning} variant=\"warning\" />}\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 onPolymerBuild={path ? handlePolymerBuild : 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\n path={path}\n workspaceCwd={tab.workspaceCwd}\n tabId={tab.id}\n isActive={isActive}\n />\n </Panel>\n );\n}\n"],"names":["moleculeSource","path","objectUrl","shouldApplyRefresh","changedAt","lastAppliedAt","modified","ERROR_MESSAGES","writeFile","client","content","response","code","POLYMER_EXTENSION","polymerFileName","args","sourcePath","monomers","attempt","suffix","polymerStem","MOLVIEWER_THEME","MAX_POLYMER_NAME_ATTEMPTS","writePolymer","upload","text","dir","dirOf","claimed","name","createEntry","err","CreateEntryError","deleteEntry","MoleculeViewer","workspaceCwd","tabId","isActive","onModifiedChange","useConnectionStore","s","useFileTransfer","download","useFileDownload","useFileWatch","handleRef","useRef","hasLoadedRef","loadError","setLoadError","useState","setModified","saveError","setSaveError","buildError","setBuildError","buildWarning","setBuildWarning","lastAppliedAtRef","_a","source","useMemo","useEffect","hasUnappliedChange","handleSave","e","handlePolymerBuild","created","pane","useLayoutStore","openMoleculeTab","errorMessage","jsx","Panel","jsxs","EmptyState","Spinner","styles","StatusBadge","MolViewer","m","MoleculeViewerPanel","tab","useIsTabVisible"],"mappings":"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"}
1
+ {"version":3,"file":"MoleculeViewerPanel-CKaIka-k.js","sources":["../../../src/features/files/molecule-source.ts","../../../src/features/files/molecule-reload.ts","../../../src/features/files/write-file.ts","../../../src/features/files/polymer-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 * Name derivation for a polymer built inside the molecule viewer (`@molviewer/core`'s\n * `onPolymerBuild`, 0.4.4+).\n *\n * Naming the file is deliberately the HOST's job. `PolymerBuildEvent` reports the monomer's name\n * and the build settings but never a target, because only the host can see the directory and so\n * only the host knows which names are already taken.\n *\n * Kept pure and separate from `MoleculeViewer.tsx` so it is testable under the root vitest runner,\n * which discovers `.test.ts` under a node environment only — same split as its siblings\n * `molecule-source.ts` and `molecule-reload.ts`.\n *\n * Building does not change what is on screen, so pressing Build repeatedly is normal and every\n * press must produce its own file. `polymerFileName` is therefore an ATTEMPT-indexed sequence,\n * walked by the caller against the create-exclusive `file_create_request` until one lands — the\n * filesystem arbitrates the collision, not a directory listing that could be stale.\n */\n\n/** Polymers always leave molviewer as MOL2: the builder CONSTRUCTS the junction bonds and marks\n * them authoritative, so a format that cannot carry a bond list would lose them outright. */\nconst POLYMER_EXTENSION = \"mol2\";\n\n/**\n * Candidate file name for attempt `attempt` (0-based) of a build off the monomer at `sourcePath`.\n *\n * Attempt 0 is `<monomer-stem>_polymer_<monomers>.mol2`; every later attempt appends its own\n * ordinal — `_2`, `_3`, … — so the trailing number reads as \"the Nth file with these settings\"\n * and the first one needs no suffix at all.\n *\n * `monomers` is the chain length in monomer units (`PolymerBuildEvent.monomers`), not the physical\n * length in Å that `report` carries.\n */\nexport function polymerFileName(args: {\n sourcePath: string;\n monomers: number;\n attempt: number;\n}): string {\n const { sourcePath, monomers, attempt } = args;\n const suffix = attempt > 0 ? `_${attempt + 1}` : \"\";\n return `${polymerStem(sourcePath)}_polymer_${monomers}${suffix}.${POLYMER_EXTENSION}`;\n}\n\n/**\n * Base name of the monomer with its directory and extension stripped.\n *\n * Taken from the tab's own absolute path rather than `PolymerBuildEvent.sourceFileName` for the\n * same reason the save path ignores `SaveEvent.fileName`: the event carries molviewer's load-time\n * basename, while the path prop is the authoritative location the tab was opened from. It also\n * makes the event's `sourceFileName: null` case (a monomer drawn in the viewer) unreachable here —\n * that tab has no path, so it never gets a build handler in the first place.\n *\n * Falls back to `polymer` for a name that is entirely extension (`.pdb`), which would otherwise\n * produce a leading-underscore stem.\n */\nfunction polymerStem(sourcePath: string): string {\n const base = sourcePath.split(\"/\").pop() ?? \"\";\n return base.replace(/\\.[^.]+$/, \"\").trim() || \"polymer\";\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.3+): 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 * Polymer build (`onPolymerBuild`, `@molviewer/core` 0.4.4+): file-backed tabs only, like `onSave`\n * — but omitting it does NOT hide the Build button the way omitting `onSave` hides Save. With no\n * handler molviewer downloads the .mol2 itself, which is the right answer for a `path`-null\n * (\"+\"-menu) tab: there is no directory to write beside.\n *\n * Building deliberately does not change what is on screen — the monomer stays loaded and the\n * polymer is a NEW structure — so this writes it beside the monomer and opens it in its own\n * molecule tab, in the SAME pane this viewer lives in. See `writePolymer` below for the naming\n * and rollback contract.\n */\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n MolViewer,\n type MolViewerHandle,\n type PolymerBuildEvent,\n type SaveEvent,\n} from \"@molviewer/core\";\nimport \"@molviewer/core/style.css\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\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 { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { dirOf } from \"@pi-studio-ui/lib/paths.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { useFileWatch } from \"@pi-studio-ui/hooks/use-file-watch.js\";\nimport { useFileTransfer } from \"@pi-studio-ui/hooks/use-file-transfer.js\";\nimport { moleculeSource } from \"./molecule-source.js\";\nimport { shouldApplyRefresh } from \"./molecule-reload.js\";\nimport { writeFile } from \"./write-file.js\";\nimport { createEntry, CreateEntryError } from \"./create-entry.js\";\nimport { deleteEntry } from \"./delete-entry.js\";\nimport { openMoleculeTab } from \"./open-file-tab.js\";\nimport { polymerFileName } from \"./polymer-file.js\";\nimport { MOLVIEWER_THEME } from \"./molecule-theme.js\";\nimport styles from \"./MoleculeViewer.module.css\";\n\n/** How many `<stem>_polymer_<n>[_k].mol2` candidates to claim before giving up on a free name. A\n * ceiling only: every attempt past the first means that many builds already landed in this folder\n * with these settings, and 50 of them is a user problem rather than a naming one. */\nconst MAX_POLYMER_NAME_ATTEMPTS = 50;\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 /** Workspace this viewer's tab belongs to — needed to open the polymer's tab in the same\n * workspace. Same normalized cwd the tab was minted with, never re-derived from the active\n * session. */\n workspaceCwd: string;\n /** This viewer's own tab id, used to resolve which pane a built polymer's tab should join.\n * Without it the polymer would land in the FOCUSED pane, which is not necessarily the one the\n * monomer is being viewed in. */\n tabId: string;\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\n/**\n * Claim a free name next to `sourcePath` and fill it with `text`; returns the path written.\n *\n * The name is claimed with `file_create_request` rather than chosen from a directory listing:\n * that RPC opens `wx` (create-exclusive), so the filesystem itself arbitrates the collision and a\n * stale listing can never cause an overwrite. Each `exists` just advances to the next candidate.\n *\n * Content then goes through the binary upload stream, not `file_write_request`, because the latter\n * caps at `MAX_INLINE_FILE_READ_BYTES` (5 MiB) — reachable for a long chain of a large monomer,\n * and a build silently failing at some atom count is not a behaviour worth shipping. The upload\n * opens the target `\"w\"`, so it fills the empty file the claim just created.\n *\n * A failed upload rolls the claim back: without it every failure would litter the folder with a\n * 0-byte .mol2 that also burns its candidate name for good. The rollback is best-effort — the\n * upload error is what the user needs to see, and a delete that also fails must not mask it.\n */\nasync function writePolymer(args: {\n client: PiStudioClient;\n upload: (dir: string, file: File) => Promise<void>;\n sourcePath: string;\n monomers: number;\n text: string;\n}): Promise<string> {\n const { client, upload, sourcePath, monomers, text } = args;\n const dir = dirOf(sourcePath);\n\n let claimed: string | null = null;\n let name = \"\";\n for (let attempt = 0; attempt < MAX_POLYMER_NAME_ATTEMPTS && claimed === null; attempt += 1) {\n name = polymerFileName({ sourcePath, monomers, attempt });\n try {\n claimed = await createEntry(client, dir, name, \"file\");\n } catch (err) {\n if (err instanceof CreateEntryError && err.code === \"exists\") continue;\n throw err;\n }\n }\n if (claimed === null)\n throw new Error(`No free name for ${name} — too many builds in this folder.`);\n\n try {\n await upload(dir, new File([text], name, { type: \"chemical/x-mol2\" }));\n } catch (err) {\n await deleteEntry(client, claimed).catch(() => {});\n throw err;\n }\n return claimed;\n}\n\nexport function MoleculeViewer({\n path,\n workspaceCwd,\n tabId,\n isActive,\n onModifiedChange,\n}: MoleculeViewerProps) {\n const client = useConnectionStore((s) => s.client);\n const { upload } = useFileTransfer();\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 const [buildError, setBuildError] = useState<string | null>(null);\n const [buildWarning, setBuildWarning] = 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 /**\n * A polymer was built. Nothing on screen changes — the monomer stays loaded — so the whole job\n * here is getting the new structure onto disk beside it and into its own tab.\n *\n * `e.text()` is a thunk and is called exactly once, up front: serialising a long chain costs\n * real time, and calling it after the name is claimed would leave an empty file behind if\n * serialisation threw.\n *\n * The new tab joins THIS viewer's pane (`paneOfTab`) rather than the focused one, so a build\n * started in a background pane doesn't fling its result across the workspace. `openMoleculeTab`\n * treats an unknown pane id as \"not supplied\" and falls back to the focused pane, so a null from\n * `paneOfTab` needs no special case.\n */\n async function handlePolymerBuild(e: PolymerBuildEvent) {\n if (!path) return;\n if (!client) {\n setBuildError(\"Not connected — cannot save the polymer.\");\n return;\n }\n setBuildError(null);\n setBuildWarning(null);\n try {\n const created = await writePolymer({\n client,\n upload,\n sourcePath: path,\n monomers: e.monomers,\n text: e.text(),\n });\n // Rigid placement is never minimised, so overlapping atoms are a real result the user has to\n // know about before feeding this into anything. Surfaced after the write, not instead of it:\n // the file is still valid and still theirs.\n if (e.report.clashes > 0) {\n setBuildWarning(\n `${e.report.clashes} close contacts — relax the geometry before simulating`,\n );\n }\n const pane = useLayoutStore.getState().paneOfTab(workspaceCwd, tabId);\n openMoleculeTab(created, workspaceCwd, pane ?? undefined);\n } catch (err) {\n setBuildError(err instanceof Error ? err.message : \"Failed to save the polymer\");\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 {buildError && <StatusBadge label={`Polymer build: ${buildError}`} variant=\"error\" />}\n {buildWarning && <StatusBadge label={buildWarning} variant=\"warning\" />}\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 onPolymerBuild={path ? handlePolymerBuild : 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\n path={path}\n workspaceCwd={tab.workspaceCwd}\n tabId={tab.id}\n isActive={isActive}\n />\n </Panel>\n );\n}\n"],"names":["moleculeSource","path","objectUrl","shouldApplyRefresh","changedAt","lastAppliedAt","modified","ERROR_MESSAGES","writeFile","client","content","response","code","POLYMER_EXTENSION","polymerFileName","args","sourcePath","monomers","attempt","suffix","polymerStem","MOLVIEWER_THEME","MAX_POLYMER_NAME_ATTEMPTS","writePolymer","upload","text","dir","dirOf","claimed","name","createEntry","err","CreateEntryError","deleteEntry","MoleculeViewer","workspaceCwd","tabId","isActive","onModifiedChange","useConnectionStore","s","useFileTransfer","download","useFileDownload","useFileWatch","handleRef","useRef","hasLoadedRef","loadError","setLoadError","useState","setModified","saveError","setSaveError","buildError","setBuildError","buildWarning","setBuildWarning","lastAppliedAtRef","_a","source","useMemo","useEffect","hasUnappliedChange","handleSave","e","handlePolymerBuild","created","pane","useLayoutStore","openMoleculeTab","errorMessage","jsx","Panel","jsxs","EmptyState","Spinner","styles","StatusBadge","MolViewer","m","MoleculeViewerPanel","tab","useIsTabVisible"],"mappings":"wmBASO,SAASA,EACdC,EACAC,EACwB,CACxB,MAAI,CAACD,GAAQ,CAACC,EAAkB,KACzB,CAAE,IAAKA,EAAW,KAAMD,EAAK,MAAM,GAAG,EAAE,IAAA,GAASA,CAAA,CAC1D,CCIO,SAASE,EAAmB,CACjC,UAAAC,EACA,cAAAC,EACA,SAAAC,CACF,EAAoC,CAGlC,MAFI,EAAAF,IAAc,MACdE,GACAF,IAAcC,EAEpB,CClBA,MAAME,EAAyC,CAC7C,WAAY,mBACZ,UAAW,0CACX,UAAW,6BACX,WAAY,8BACZ,WAAY,0BACd,EAIA,eAAsBC,GACpBC,EACAR,EACAS,EACe,CACf,MAAMC,EAAW,MAAMF,EAAO,WAAW,QACvC,qBACA,CAAE,KAAAR,EAAM,QAAAS,CAAA,CAAQ,EAElB,GAAI,CAACC,EAAS,GAAI,CAChB,MAAMC,EAAOD,EAAS,OAAS,GAC/B,MAAM,IAAI,MAAMJ,EAAeK,CAAI,GAAKA,GAAQ,gBAAgB,CAClE,CACF,CCbA,MAAMC,GAAoB,OAYnB,SAASC,GAAgBC,EAIrB,CACT,KAAM,CAAE,WAAAC,EAAY,SAAAC,EAAU,QAAAC,CAAA,EAAYH,EACpCI,EAASD,EAAU,EAAI,IAAIA,EAAU,CAAC,GAAK,GACjD,MAAO,GAAGE,GAAYJ,CAAU,CAAC,YAAYC,CAAQ,GAAGE,CAAM,IAAIN,EAAiB,EACrF,CAcA,SAASO,GAAYJ,EAA4B,CAE/C,OADaA,EAAW,MAAM,GAAG,EAAE,OAAS,IAChC,QAAQ,WAAY,EAAE,EAAE,QAAU,SAChD,CC5BO,MAAMK,GAA0C,CAErD,WAAY,2BACZ,SAAU,2BACV,WAAY,2BACZ,WAAY,2BAEZ,YAAa,2BACb,kBAAmB,2BACnB,yBAA0B,2BAC1B,mBAAoB,yBACpB,iBAAkB,yBAElB,YAAa,2BACb,uBAAwB,2BACxB,cAAe,2BACf,mBAAoB,yBAEpB,iBAAkB,6BAClB,oBAAqB,4BACvB,qGCoBMC,GAA4B,GAwClC,eAAeC,GAAaR,EAMR,CAClB,KAAM,CAAE,OAAAN,EAAQ,OAAAe,EAAQ,WAAAR,EAAY,SAAAC,EAAU,KAAAQ,GAASV,EACjDW,EAAMC,EAAMX,CAAU,EAE5B,IAAIY,EAAyB,KACzBC,EAAO,GACX,QAASX,EAAU,EAAGA,EAAUI,IAA6BM,IAAY,KAAMV,GAAW,EAAG,CAC3FW,EAAOf,GAAgB,CAAE,WAAAE,EAAY,SAAAC,EAAU,QAAAC,EAAS,EACxD,GAAI,CACFU,EAAU,MAAME,EAAYrB,EAAQiB,EAAKG,EAAM,MAAM,CACvD,OAASE,EAAK,CACZ,GAAIA,aAAeC,GAAoBD,EAAI,OAAS,SAAU,SAC9D,MAAMA,CACR,CACF,CACA,GAAIH,IAAY,KACd,MAAM,IAAI,MAAM,oBAAoBC,CAAI,oCAAoC,EAE9E,GAAI,CACF,MAAML,EAAOE,EAAK,IAAI,KAAK,CAACD,CAAI,EAAGI,EAAM,CAAE,KAAM,iBAAA,CAAmB,CAAC,CACvE,OAASE,EAAK,CACZ,YAAME,EAAYxB,EAAQmB,CAAO,EAAE,MAAM,IAAM,CAAC,CAAC,EAC3CG,CACR,CACA,OAAOH,CACT,CAEO,SAASM,GAAe,CAC7B,KAAAjC,EACA,aAAAkC,EACA,MAAAC,EACA,SAAAC,EACA,iBAAAC,CACF,EAAwB,OACtB,MAAM7B,EAAS8B,EAAoBC,GAAMA,EAAE,MAAM,EAC3C,CAAE,OAAAhB,CAAA,EAAWiB,EAAA,EACbC,EAAWC,EAAgB1C,GAAQ,GAAI,EAAQA,CAAK,EACpD,CAAE,UAAAG,CAAA,EAAcwC,EAAa3C,CAAI,EACjC4C,EAAYC,EAAAA,OAAwB,IAAI,EAKxCC,EAAeD,EAAAA,OAAO,EAAK,EAC3B,CAACE,EAAWC,CAAY,EAAIC,EAAAA,SAAuB,IAAI,EACvD,CAAC5C,EAAU6C,CAAW,EAAID,EAAAA,SAAS,EAAK,EACxC,CAACE,EAAWC,CAAY,EAAIH,EAAAA,SAAwB,IAAI,EACxD,CAACI,EAAYC,CAAa,EAAIL,EAAAA,SAAwB,IAAI,EAC1D,CAACM,EAAcC,CAAe,EAAIP,EAAAA,SAAwB,IAAI,EAG9DQ,EAAmBZ,EAAAA,OAAsB,IAAI,EAa7C5C,IAAYyD,EAAAjB,EAAS,OAAT,YAAAiB,EAAe,YAAa,KACxCC,EAASC,UAAQ,IAAM7D,EAAeC,EAAMC,CAAS,EAAG,CAACD,EAAMC,CAAS,CAAC,EAE/E4D,EAAAA,UAAU,IAAM,CACT3D,EAAmB,CAAE,UAAAC,EAAW,cAAesD,EAAiB,QAAS,SAAApD,CAAA,CAAU,IAGxFoD,EAAiB,QAAUtD,EACtBsC,EAAS,QAAA,EAIhB,EAAG,CAACtC,EAAWE,EAAUoC,EAAS,OAAO,CAAC,EAK1C,MAAMqB,EACJ3D,IAAc,MAAQA,IAAcsD,EAAiB,SAAWpD,EAElE,eAAe0D,EAAWC,EAAc,CACtC,GAAKhE,EACL,IAAI,CAACQ,EAAQ,CACX4C,EAAa,8BAA8B,EAC3C,MACF,CACAA,EAAa,IAAI,EACjB,GAAI,CACF,MAAM7C,GAAUC,EAAQR,EAAMgE,EAAE,MAAM,EACtCA,EAAE,MAAA,CACJ,OAASlC,EAAK,CACZsB,EAAatB,aAAe,MAAQA,EAAI,QAAU,gBAAgB,CACpE,EACF,CAeA,eAAemC,EAAmBD,EAAsB,CACtD,GAAKhE,EACL,IAAI,CAACQ,EAAQ,CACX8C,EAAc,0CAA0C,EACxD,MACF,CACAA,EAAc,IAAI,EAClBE,EAAgB,IAAI,EACpB,GAAI,CACF,MAAMU,EAAU,MAAM5C,GAAa,CACjC,OAAAd,EACA,OAAAe,EACA,WAAYvB,EACZ,SAAUgE,EAAE,SACZ,KAAMA,EAAE,KAAA,CAAK,CACd,EAIGA,EAAE,OAAO,QAAU,GACrBR,EACE,GAAGQ,EAAE,OAAO,OAAO,wDAAA,EAGvB,MAAMG,EAAOC,EAAe,SAAA,EAAW,UAAUlC,EAAcC,CAAK,EACpEkC,EAAgBH,EAAShC,EAAciC,GAAQ,MAAS,CAC1D,OAASrC,EAAK,CACZwB,EAAcxB,aAAe,MAAQA,EAAI,QAAU,4BAA4B,CACjF,EACF,CAOA,MAAMwC,GALuB7B,EAAS,QAClCA,EAAS,iBAAiB,MACxBA,EAAS,MAAM,QACf,kBACF,QACyCM,GAAA,YAAAA,EAAW,UAAW,KAEnE,OAAI/C,GAAQyC,EAAS,UAEjB8B,EAAAA,IAACC,EAAA,CACC,SAAAC,EAAAA,KAACC,EAAA,CACC,SAAA,CAAAH,EAAAA,IAACI,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,CAAA,CACvB,CAAA,CACF,EAGAL,EAEAC,EAAAA,IAACC,EAAA,CACC,SAAAC,EAAAA,KAACC,EAAA,CAAW,SAAA,CAAA,UAAQJ,CAAA,CAAA,CAAa,CAAA,CACnC,EAKFG,OAACD,GAAM,UAAWI,EAAO,KAAM,uBAAsBxC,EAAW,OAAS,QACvE,SAAA,CAAAqC,EAAAA,KAAC,MAAA,CAAI,UAAWG,EAAO,OACpB,SAAA,CAAAd,GAAsBS,EAAAA,IAACM,EAAA,CAAY,MAAM,uBAAuB,QAAQ,QAAQ,EAChF1B,SAAc0B,EAAA,CAAY,MAAO,gBAAgB1B,CAAS,GAAI,QAAQ,QAAQ,EAC9EE,SAAewB,EAAA,CAAY,MAAO,kBAAkBxB,CAAU,GAAI,QAAQ,QAAQ,EAClFE,GAAgBgB,EAAAA,IAACM,EAAA,CAAY,MAAOtB,EAAc,QAAQ,SAAA,CAAU,CAAA,EACvE,EACAgB,EAAAA,IAACO,EAAA,CACC,IAAKlC,EACL,UAAWgC,EAAO,UAClB,MAAOxD,GACP,OAAAuC,EACA,WAAYb,EAAa,QAAU,SAAW,UAC9C,OAAQ,IAAM,CACZA,EAAa,QAAU,GACvBE,EAAa,IAAI,CACnB,EACA,YAAcgB,GAAMhB,EAAagB,EAAE,KAAK,EACxC,iBAAmBe,GAAM,CACvB7B,EAAY6B,CAAC,EACb1C,GAAA,MAAAA,EAAmB0C,EACrB,EACA,OAAQ/E,EAAO+D,EAAa,OAC5B,eAAgB/D,EAAOiE,EAAqB,MAAA,CAAA,CAC9C,EACF,CAEJ,CC1SO,SAASe,GAAoB,CAAE,IAAAC,GAAmB,CACvD,KAAM,CAAE,KAAAjF,GAASiF,EAAI,KAEf7C,EAAW8C,EAAgBD,EAAI,EAAE,EAEvC,aACGT,EAAA,CACC,SAAAD,EAAAA,IAACtC,GAAA,CACC,KAAAjC,EACA,aAAciF,EAAI,aAClB,MAAOA,EAAI,GACX,SAAA7C,CAAA,CAAA,EAEJ,CAEJ"}
@@ -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-Uv3j2inr.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-CVbpa3Qk.js";import"./vendor-BLc7PaxD.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-markdown-DtEbK_xy.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-aflKubq7.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-Uv3j2inr.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-Cz7AgOgN.js";import"./vendor-BLc7PaxD.js";import"./vendor-query-NxjRJKdp.js";import"./vendor-icons-DDiHMdgo.js";import"./vendor-overlays-Ki1AFMsh.js";import"./vendor-dnd-Bvh3agTi.js";import"./vendor-highlight-CJuJYF7f.js";import"./vendor-markdown-DtEbK_xy.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-B7ujFxCG.js.map