@av-pi-studio/web-client 0.0.45 → 0.0.47

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 (46) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/web/assets/{BinaryFallbackViewer-DN2oT6N7.js → BinaryFallbackViewer-B-iTQZ_y.js} +2 -2
  3. package/dist/web/assets/{BinaryFallbackViewer-DN2oT6N7.js.map → BinaryFallbackViewer-B-iTQZ_y.js.map} +1 -1
  4. package/dist/web/assets/ChatPanel-BFSx14-w.js +3 -0
  5. package/dist/web/assets/ChatPanel-BFSx14-w.js.map +1 -0
  6. package/dist/web/assets/{CodeView-DwUKpXwU.js → CodeView-DRJLXH-w.js} +2 -2
  7. package/dist/web/assets/{CodeView-DwUKpXwU.js.map → CodeView-DRJLXH-w.js.map} +1 -1
  8. package/dist/web/assets/FilePanel-BrbOLTdf.js +2 -0
  9. package/dist/web/assets/FilePanel-BrbOLTdf.js.map +1 -0
  10. package/dist/web/assets/{ImageViewer-BKflylHn.js → ImageViewer-B82Lth8R.js} +2 -2
  11. package/dist/web/assets/{ImageViewer-BKflylHn.js.map → ImageViewer-B82Lth8R.js.map} +1 -1
  12. package/dist/web/assets/MarkdownFileViewer-uPA7NmW6.js +2 -0
  13. package/dist/web/assets/{MarkdownFileViewer-BDmo3la-.js.map → MarkdownFileViewer-uPA7NmW6.js.map} +1 -1
  14. package/dist/web/assets/{MoleculeViewerPanel-BZA00vSx.js → MoleculeViewerPanel-Ct9B4hGp.js} +2 -2
  15. package/dist/web/assets/{MoleculeViewerPanel-BZA00vSx.js.map → MoleculeViewerPanel-Ct9B4hGp.js.map} +1 -1
  16. package/dist/web/assets/{TerminalPanel-qx_HdM7y.js → TerminalPanel-CXsU1Xtg.js} +2 -2
  17. package/dist/web/assets/{TerminalPanel-qx_HdM7y.js.map → TerminalPanel-CXsU1Xtg.js.map} +1 -1
  18. package/dist/web/assets/{TextViewer-CDXNfUrU.js → TextViewer-DHDR_OoU.js} +2 -2
  19. package/dist/web/assets/{TextViewer-CDXNfUrU.js.map → TextViewer-DHDR_OoU.js.map} +1 -1
  20. package/dist/web/assets/{VideoViewer-D4XJC0lT.js → VideoViewer-Bv9fQkLE.js} +2 -2
  21. package/dist/web/assets/{VideoViewer-D4XJC0lT.js.map → VideoViewer-Bv9fQkLE.js.map} +1 -1
  22. package/dist/web/assets/index-Bu36Yj2q.css +1 -0
  23. package/dist/web/assets/index-DulchPgH.js +3 -0
  24. package/dist/web/assets/index-DulchPgH.js.map +1 -0
  25. package/dist/web/assets/markdown-C6wNVyCO.js +2 -0
  26. package/dist/web/assets/markdown-C6wNVyCO.js.map +1 -0
  27. package/dist/web/assets/markdown-JJHL50c6.css +1 -0
  28. package/dist/web/assets/paths--dEDMUm1.js +2 -0
  29. package/dist/web/assets/paths--dEDMUm1.js.map +1 -0
  30. package/dist/web/assets/{use-file-download-DsIqcxhB.js → use-file-download-AaY3793H.js} +2 -2
  31. package/dist/web/assets/{use-file-download-DsIqcxhB.js.map → use-file-download-AaY3793H.js.map} +1 -1
  32. package/dist/web/assets/{use-file-watch-ld_2lbTl.js → use-file-watch-C52FKLSq.js} +2 -2
  33. package/dist/web/assets/{use-file-watch-ld_2lbTl.js.map → use-file-watch-C52FKLSq.js.map} +1 -1
  34. package/dist/web/index.html +2 -2
  35. package/package.json +3 -3
  36. package/dist/web/assets/ChatPanel-YUfYdTdZ.js +0 -3
  37. package/dist/web/assets/ChatPanel-YUfYdTdZ.js.map +0 -1
  38. package/dist/web/assets/FilePanel-DCZ6Vntr.js +0 -2
  39. package/dist/web/assets/FilePanel-DCZ6Vntr.js.map +0 -1
  40. package/dist/web/assets/MarkdownFileViewer-BDmo3la-.js +0 -2
  41. package/dist/web/assets/index-BsG4HHRz.css +0 -1
  42. package/dist/web/assets/index-DneBC_ta.js +0 -3
  43. package/dist/web/assets/index-DneBC_ta.js.map +0 -1
  44. package/dist/web/assets/markdown-BQwXm0MF.css +0 -1
  45. package/dist/web/assets/markdown-BVrzq29V.js +0 -2
  46. package/dist/web/assets/markdown-BVrzq29V.js.map +0 -1
@@ -1,2 +1,2 @@
1
- var f=Object.defineProperty;var d=(e,r,t)=>r in e?f(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var n=(e,r,t)=>d(e,typeof r!="symbol"?r+"":r,t);import{u as m}from"./vendor-query-chxBrUuj.js";import{a as p,r as g}from"./index-DneBC_ta.js";import{j as u,r as l}from"./vendor-react-p-LLqNcx.js";import{O as h,P as w,Q as x,S as y,T as E,U as _}from"./vendor-B2Vnrid8.js";class F extends Error{constructor(t,a){super("file_too_large");n(this,"size");n(this,"maxBytes");this.name="FileTooLargeError",this.size=t,this.maxBytes=a}}function R(e){if(!e.ok)throw e.error==="file_too_large"?new F(e.size??0,e.maxBytes):new Error(e.error??"file read failed");return{content:e.content??"",size:e.size??0}}function v(e,r=!0){const t=p(a=>a.client);return m({queryKey:g.fileRead(e),queryFn:async()=>{if(!t)throw new Error("not connected");const a=await t.connection.request("file_read_request",{path:e});return R(a)},enabled:!!t&&!!e&&r})}const z="_wrap_17s3u_1",B={wrap:z},c=new Map;function C(e){const[r,t]=l.useState(null);return l.useEffect(()=>{const a=E.matchFilename(_,e);if(!a){t(null);return}const s=c.get(a.name);if(s){t(s);return}let o=!1;return t(null),a.load().then(i=>{c.set(a.name,i),o||t(i)}),()=>{o=!0}},[e]),r?[r]:[]}function K({path:e,content:r}){const t=C(e),a=[h.lineWrapping,w(),...t];return u.jsx("div",{className:B.wrap,children:u.jsx(x,{value:r,theme:y,extensions:a,editable:!1,readOnly:!0,basicSetup:{highlightActiveLine:!1,foldGutter:!1,searchKeymap:!1},height:"100%"})})}export{K as C,F,v as u};
2
- //# sourceMappingURL=CodeView-DwUKpXwU.js.map
1
+ var f=Object.defineProperty;var d=(e,r,t)=>r in e?f(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var n=(e,r,t)=>d(e,typeof r!="symbol"?r+"":r,t);import{u as m}from"./vendor-query-chxBrUuj.js";import{a as p,r as g}from"./index-DulchPgH.js";import{j as u,r as l}from"./vendor-react-p-LLqNcx.js";import{O as h,P as w,Q as x,S as y,T as E,U as _}from"./vendor-B2Vnrid8.js";class F extends Error{constructor(t,a){super("file_too_large");n(this,"size");n(this,"maxBytes");this.name="FileTooLargeError",this.size=t,this.maxBytes=a}}function R(e){if(!e.ok)throw e.error==="file_too_large"?new F(e.size??0,e.maxBytes):new Error(e.error??"file read failed");return{content:e.content??"",size:e.size??0}}function v(e,r=!0){const t=p(a=>a.client);return m({queryKey:g.fileRead(e),queryFn:async()=>{if(!t)throw new Error("not connected");const a=await t.connection.request("file_read_request",{path:e});return R(a)},enabled:!!t&&!!e&&r})}const z="_wrap_17s3u_1",B={wrap:z},c=new Map;function C(e){const[r,t]=l.useState(null);return l.useEffect(()=>{const a=E.matchFilename(_,e);if(!a){t(null);return}const s=c.get(a.name);if(s){t(s);return}let o=!1;return t(null),a.load().then(i=>{c.set(a.name,i),o||t(i)}),()=>{o=!0}},[e]),r?[r]:[]}function K({path:e,content:r}){const t=C(e),a=[h.lineWrapping,w(),...t];return u.jsx("div",{className:B.wrap,children:u.jsx(x,{value:r,theme:y,extensions:a,editable:!1,readOnly:!0,basicSetup:{highlightActiveLine:!1,foldGutter:!1,searchKeymap:!1},height:"100%"})})}export{K as C,F,v as u};
2
+ //# sourceMappingURL=CodeView-DRJLXH-w.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CodeView-DwUKpXwU.js","sources":["../../../src/hooks/use-file-read.ts","../../../src/features/files/CodeView.tsx"],"sourcesContent":["/**\n * `use-file-read` — TanStack Query wrapper over `file_read_request` (POC `loadFileContent`,\n * POC_TO_APP_PLAN_UI.md §4.5 / §3 `hooks/use-file-read.ts`).\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface FileReadResult {\n content: string;\n size: number;\n}\n\n/** Thrown by `useFileRead` when the server rejects a read with `error: \"file_too_large\"`\n * (`file_read_response`, `MAX_INLINE_FILE_READ_BYTES` server-side). Carries `size` and the\n * server's own `maxBytes` ceiling so a caller can render an accurate message and decide whether\n * to fall back to the streaming path (`use-file-text.ts`) instead of just displaying the string\n * `\"file_too_large\"`. */\nexport class FileTooLargeError extends Error {\n readonly size: number;\n readonly maxBytes?: number;\n\n constructor(size: number, maxBytes?: number) {\n super(\"file_too_large\");\n this.name = \"FileTooLargeError\";\n this.size = size;\n this.maxBytes = maxBytes;\n }\n}\n\nexport interface FileReadRpcResponse {\n ok: boolean;\n content?: string;\n size?: number;\n maxBytes?: number;\n error?: string;\n}\n\n/** Maps a raw `file_read_response` into a result or a thrown error — pure, so the\n * `file_too_large` classification (the actual behavior change in this task) is directly\n * unit-testable without a connected client or React. */\nexport function parseFileReadResponse(response: FileReadRpcResponse): FileReadResult {\n if (!response.ok) {\n if (response.error === \"file_too_large\") {\n throw new FileTooLargeError(response.size ?? 0, response.maxBytes);\n }\n throw new Error(response.error ?? \"file read failed\");\n }\n return { content: response.content ?? \"\", size: response.size ?? 0 };\n}\n\nexport function useFileRead(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.fileRead(path),\n queryFn: async (): Promise<FileReadResult> => {\n if (!client) throw new Error(\"not connected\");\n const response = await client.connection.request<FileReadRpcResponse>(\"file_read_request\", {\n path,\n });\n return parseFileReadResponse(response);\n },\n enabled: Boolean(client) && Boolean(path) && enabled,\n });\n}\n","/**\n * CodeView — highlighted, read-only file body with a line-number gutter (POC `showFile`/\n * `reloadFileInPanel`, chat.html ~line 812-828, POC_TO_APP_PLAN_UI.md §4.5). Built on CodeMirror 6\n * (`@uiw/react-codemirror`) rather than a hand-rolled `<pre>` + separate gutter `<pre>`: the two\n * previously drifted out of sync on scroll (independent scroll containers, no shared scroll\n * anchor) whenever content was long enough to scroll or lines wrapped. CodeMirror owns a single\n * scrollable viewport with gutter and content painted from the same layout pass, so this class of\n * bug cannot recur.\n */\n\nimport { useEffect, useState } from \"react\";\nimport CodeMirror from \"@uiw/react-codemirror\";\nimport type { Extension } from \"@codemirror/state\";\nimport { EditorView } from \"@codemirror/view\";\nimport { languages } from \"@codemirror/language-data\";\nimport { LanguageDescription, type LanguageSupport, foldGutter } from \"@codemirror/language\";\nimport { githubDark } from \"@uiw/codemirror-theme-github\";\nimport styles from \"./CodeView.module.css\";\n\nexport interface CodeViewProps {\n path: string;\n content: string;\n}\n\n// `LanguageDescription.support` is populated lazily by `.load()`, which dynamically imports the\n// grammar the first time a given language is used and resolves instantly (cached) after that.\nconst supportCache = new Map<string, LanguageSupport>();\n\nfunction useLanguageExtension(path: string): Extension[] {\n const [support, setSupport] = useState<LanguageSupport | null>(null);\n\n useEffect(() => {\n const desc = LanguageDescription.matchFilename(languages, path);\n if (!desc) {\n setSupport(null);\n return;\n }\n const cached = supportCache.get(desc.name);\n if (cached) {\n setSupport(cached);\n return;\n }\n let cancelled = false;\n setSupport(null);\n void desc.load().then((loaded) => {\n supportCache.set(desc.name, loaded);\n if (!cancelled) setSupport(loaded);\n });\n return () => {\n cancelled = true;\n };\n }, [path]);\n\n return support ? [support] : [];\n}\n\nexport function CodeView({ path, content }: CodeViewProps) {\n const languageExt = useLanguageExtension(path);\n const extensions: Extension[] = [EditorView.lineWrapping, foldGutter(), ...languageExt];\n\n return (\n <div className={styles.wrap}>\n <CodeMirror\n value={content}\n theme={githubDark}\n extensions={extensions}\n editable={false}\n readOnly\n basicSetup={{\n highlightActiveLine: false,\n foldGutter: false,\n searchKeymap: false,\n }}\n height=\"100%\"\n />\n </div>\n );\n}\n"],"names":["FileTooLargeError","size","maxBytes","__publicField","parseFileReadResponse","response","useFileRead","path","enabled","client","useConnectionStore","s","useQuery","rpcKeys","supportCache","useLanguageExtension","support","setSupport","useState","useEffect","desc","LanguageDescription","languages","cached","cancelled","loaded","CodeView","content","languageExt","extensions","EditorView","foldGutter","jsx","styles","CodeMirror","githubDark"],"mappings":"oYAmBO,MAAMA,UAA0B,KAAM,CAI3C,YAAYC,EAAcC,EAAmB,CAC3C,MAAM,gBAAgB,EAJfC,EAAA,aACAA,EAAA,iBAIP,KAAK,KAAO,oBACZ,KAAK,KAAOF,EACZ,KAAK,SAAWC,CAClB,CACF,CAaO,SAASE,EAAsBC,EAA+C,CACnF,GAAI,CAACA,EAAS,GACZ,MAAIA,EAAS,QAAU,iBACf,IAAIL,EAAkBK,EAAS,MAAQ,EAAGA,EAAS,QAAQ,EAE7D,IAAI,MAAMA,EAAS,OAAS,kBAAkB,EAEtD,MAAO,CAAE,QAASA,EAAS,SAAW,GAAI,KAAMA,EAAS,MAAQ,CAAA,CACnE,CAEO,SAASC,EAAYC,EAAcC,EAAU,GAAM,CACxD,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAEjD,OAAOC,EAAS,CACd,SAAUC,EAAQ,SAASN,CAAI,EAC/B,QAAS,SAAqC,CAC5C,GAAI,CAACE,EAAQ,MAAM,IAAI,MAAM,eAAe,EAC5C,MAAMJ,EAAW,MAAMI,EAAO,WAAW,QAA6B,oBAAqB,CACzF,KAAAF,CAAA,CACD,EACD,OAAOH,EAAsBC,CAAQ,CACvC,EACA,QAAS,EAAQI,GAAW,EAAQF,GAASC,CAAA,CAC9C,CACH,oCCxCMM,MAAmB,IAEzB,SAASC,EAAqBR,EAA2B,CACvD,KAAM,CAACS,EAASC,CAAU,EAAIC,EAAAA,SAAiC,IAAI,EAEnEC,OAAAA,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAOC,EAAoB,cAAcC,EAAWf,CAAI,EAC9D,GAAI,CAACa,EAAM,CACTH,EAAW,IAAI,EACf,MACF,CACA,MAAMM,EAAST,EAAa,IAAIM,EAAK,IAAI,EACzC,GAAIG,EAAQ,CACVN,EAAWM,CAAM,EACjB,MACF,CACA,IAAIC,EAAY,GAChB,OAAAP,EAAW,IAAI,EACVG,EAAK,KAAA,EAAO,KAAMK,GAAW,CAChCX,EAAa,IAAIM,EAAK,KAAMK,CAAM,EAC7BD,GAAWP,EAAWQ,CAAM,CACnC,CAAC,EACM,IAAM,CACXD,EAAY,EACd,CACF,EAAG,CAACjB,CAAI,CAAC,EAEFS,EAAU,CAACA,CAAO,EAAI,CAAA,CAC/B,CAEO,SAASU,EAAS,CAAE,KAAAnB,EAAM,QAAAoB,GAA0B,CACzD,MAAMC,EAAcb,EAAqBR,CAAI,EACvCsB,EAA0B,CAACC,EAAW,aAAcC,EAAA,EAAc,GAAGH,CAAW,EAEtF,OACEI,EAAAA,IAAC,MAAA,CAAI,UAAWC,EAAO,KACrB,SAAAD,EAAAA,IAACE,EAAA,CACC,MAAOP,EACP,MAAOQ,EACP,WAAAN,EACA,SAAU,GACV,SAAQ,GACR,WAAY,CACV,oBAAqB,GACrB,WAAY,GACZ,aAAc,EAAA,EAEhB,OAAO,MAAA,CAAA,EAEX,CAEJ"}
1
+ {"version":3,"file":"CodeView-DRJLXH-w.js","sources":["../../../src/hooks/use-file-read.ts","../../../src/features/files/CodeView.tsx"],"sourcesContent":["/**\n * `use-file-read` — TanStack Query wrapper over `file_read_request` (POC `loadFileContent`,\n * POC_TO_APP_PLAN_UI.md §4.5 / §3 `hooks/use-file-read.ts`).\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface FileReadResult {\n content: string;\n size: number;\n}\n\n/** Thrown by `useFileRead` when the server rejects a read with `error: \"file_too_large\"`\n * (`file_read_response`, `MAX_INLINE_FILE_READ_BYTES` server-side). Carries `size` and the\n * server's own `maxBytes` ceiling so a caller can render an accurate message and decide whether\n * to fall back to the streaming path (`use-file-text.ts`) instead of just displaying the string\n * `\"file_too_large\"`. */\nexport class FileTooLargeError extends Error {\n readonly size: number;\n readonly maxBytes?: number;\n\n constructor(size: number, maxBytes?: number) {\n super(\"file_too_large\");\n this.name = \"FileTooLargeError\";\n this.size = size;\n this.maxBytes = maxBytes;\n }\n}\n\nexport interface FileReadRpcResponse {\n ok: boolean;\n content?: string;\n size?: number;\n maxBytes?: number;\n error?: string;\n}\n\n/** Maps a raw `file_read_response` into a result or a thrown error — pure, so the\n * `file_too_large` classification (the actual behavior change in this task) is directly\n * unit-testable without a connected client or React. */\nexport function parseFileReadResponse(response: FileReadRpcResponse): FileReadResult {\n if (!response.ok) {\n if (response.error === \"file_too_large\") {\n throw new FileTooLargeError(response.size ?? 0, response.maxBytes);\n }\n throw new Error(response.error ?? \"file read failed\");\n }\n return { content: response.content ?? \"\", size: response.size ?? 0 };\n}\n\nexport function useFileRead(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.fileRead(path),\n queryFn: async (): Promise<FileReadResult> => {\n if (!client) throw new Error(\"not connected\");\n const response = await client.connection.request<FileReadRpcResponse>(\"file_read_request\", {\n path,\n });\n return parseFileReadResponse(response);\n },\n enabled: Boolean(client) && Boolean(path) && enabled,\n });\n}\n","/**\n * CodeView — highlighted, read-only file body with a line-number gutter (POC `showFile`/\n * `reloadFileInPanel`, chat.html ~line 812-828, POC_TO_APP_PLAN_UI.md §4.5). Built on CodeMirror 6\n * (`@uiw/react-codemirror`) rather than a hand-rolled `<pre>` + separate gutter `<pre>`: the two\n * previously drifted out of sync on scroll (independent scroll containers, no shared scroll\n * anchor) whenever content was long enough to scroll or lines wrapped. CodeMirror owns a single\n * scrollable viewport with gutter and content painted from the same layout pass, so this class of\n * bug cannot recur.\n */\n\nimport { useEffect, useState } from \"react\";\nimport CodeMirror from \"@uiw/react-codemirror\";\nimport type { Extension } from \"@codemirror/state\";\nimport { EditorView } from \"@codemirror/view\";\nimport { languages } from \"@codemirror/language-data\";\nimport { LanguageDescription, type LanguageSupport, foldGutter } from \"@codemirror/language\";\nimport { githubDark } from \"@uiw/codemirror-theme-github\";\nimport styles from \"./CodeView.module.css\";\n\nexport interface CodeViewProps {\n path: string;\n content: string;\n}\n\n// `LanguageDescription.support` is populated lazily by `.load()`, which dynamically imports the\n// grammar the first time a given language is used and resolves instantly (cached) after that.\nconst supportCache = new Map<string, LanguageSupport>();\n\nfunction useLanguageExtension(path: string): Extension[] {\n const [support, setSupport] = useState<LanguageSupport | null>(null);\n\n useEffect(() => {\n const desc = LanguageDescription.matchFilename(languages, path);\n if (!desc) {\n setSupport(null);\n return;\n }\n const cached = supportCache.get(desc.name);\n if (cached) {\n setSupport(cached);\n return;\n }\n let cancelled = false;\n setSupport(null);\n void desc.load().then((loaded) => {\n supportCache.set(desc.name, loaded);\n if (!cancelled) setSupport(loaded);\n });\n return () => {\n cancelled = true;\n };\n }, [path]);\n\n return support ? [support] : [];\n}\n\nexport function CodeView({ path, content }: CodeViewProps) {\n const languageExt = useLanguageExtension(path);\n const extensions: Extension[] = [EditorView.lineWrapping, foldGutter(), ...languageExt];\n\n return (\n <div className={styles.wrap}>\n <CodeMirror\n value={content}\n theme={githubDark}\n extensions={extensions}\n editable={false}\n readOnly\n basicSetup={{\n highlightActiveLine: false,\n foldGutter: false,\n searchKeymap: false,\n }}\n height=\"100%\"\n />\n </div>\n );\n}\n"],"names":["FileTooLargeError","size","maxBytes","__publicField","parseFileReadResponse","response","useFileRead","path","enabled","client","useConnectionStore","s","useQuery","rpcKeys","supportCache","useLanguageExtension","support","setSupport","useState","useEffect","desc","LanguageDescription","languages","cached","cancelled","loaded","CodeView","content","languageExt","extensions","EditorView","foldGutter","jsx","styles","CodeMirror","githubDark"],"mappings":"oYAmBO,MAAMA,UAA0B,KAAM,CAI3C,YAAYC,EAAcC,EAAmB,CAC3C,MAAM,gBAAgB,EAJfC,EAAA,aACAA,EAAA,iBAIP,KAAK,KAAO,oBACZ,KAAK,KAAOF,EACZ,KAAK,SAAWC,CAClB,CACF,CAaO,SAASE,EAAsBC,EAA+C,CACnF,GAAI,CAACA,EAAS,GACZ,MAAIA,EAAS,QAAU,iBACf,IAAIL,EAAkBK,EAAS,MAAQ,EAAGA,EAAS,QAAQ,EAE7D,IAAI,MAAMA,EAAS,OAAS,kBAAkB,EAEtD,MAAO,CAAE,QAASA,EAAS,SAAW,GAAI,KAAMA,EAAS,MAAQ,CAAA,CACnE,CAEO,SAASC,EAAYC,EAAcC,EAAU,GAAM,CACxD,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAEjD,OAAOC,EAAS,CACd,SAAUC,EAAQ,SAASN,CAAI,EAC/B,QAAS,SAAqC,CAC5C,GAAI,CAACE,EAAQ,MAAM,IAAI,MAAM,eAAe,EAC5C,MAAMJ,EAAW,MAAMI,EAAO,WAAW,QAA6B,oBAAqB,CACzF,KAAAF,CAAA,CACD,EACD,OAAOH,EAAsBC,CAAQ,CACvC,EACA,QAAS,EAAQI,GAAW,EAAQF,GAASC,CAAA,CAC9C,CACH,oCCxCMM,MAAmB,IAEzB,SAASC,EAAqBR,EAA2B,CACvD,KAAM,CAACS,EAASC,CAAU,EAAIC,EAAAA,SAAiC,IAAI,EAEnEC,OAAAA,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAOC,EAAoB,cAAcC,EAAWf,CAAI,EAC9D,GAAI,CAACa,EAAM,CACTH,EAAW,IAAI,EACf,MACF,CACA,MAAMM,EAAST,EAAa,IAAIM,EAAK,IAAI,EACzC,GAAIG,EAAQ,CACVN,EAAWM,CAAM,EACjB,MACF,CACA,IAAIC,EAAY,GAChB,OAAAP,EAAW,IAAI,EACVG,EAAK,KAAA,EAAO,KAAMK,GAAW,CAChCX,EAAa,IAAIM,EAAK,KAAMK,CAAM,EAC7BD,GAAWP,EAAWQ,CAAM,CACnC,CAAC,EACM,IAAM,CACXD,EAAY,EACd,CACF,EAAG,CAACjB,CAAI,CAAC,EAEFS,EAAU,CAACA,CAAO,EAAI,CAAA,CAC/B,CAEO,SAASU,EAAS,CAAE,KAAAnB,EAAM,QAAAoB,GAA0B,CACzD,MAAMC,EAAcb,EAAqBR,CAAI,EACvCsB,EAA0B,CAACC,EAAW,aAAcC,EAAA,EAAc,GAAGH,CAAW,EAEtF,OACEI,EAAAA,IAAC,MAAA,CAAI,UAAWC,EAAO,KACrB,SAAAD,EAAAA,IAACE,EAAA,CACC,MAAOP,EACP,MAAOQ,EACP,WAAAN,EACA,SAAU,GACV,SAAQ,GACR,WAAY,CACV,oBAAqB,GACrB,WAAY,GACZ,aAAc,EAAA,EAEhB,OAAO,MAAA,CAAA,EAEX,CAEJ"}
@@ -0,0 +1,2 @@
1
+ import{r as f,j as s}from"./vendor-react-p-LLqNcx.js";import{C as j}from"./vendor-B2Vnrid8.js";import{a as g,r as l,d as w,B as h,V as E,S as x}from"./index-DulchPgH.js";import{u as D,c as N}from"./vendor-query-chxBrUuj.js";import{r as S}from"./paths--dEDMUm1.js";import{u as K}from"./use-file-watch-C52FKLSq.js";import{D as k}from"./DiffView-CUxhzW_O.js";import{p as r}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";function q(e,o,a,i=!0){const n=g(t=>t.client);return D({queryKey:l.fileDiff(e,o,a),queryFn:async()=>{if(!n)throw new Error("not connected");return{patch:(await n.connection.request("file_diff_request",{path:e,cwd:o,staged:a})).patch??""}},enabled:!!n&&!!e&&!!o&&i})}const B=new Set(["text","markdown","image"]);function C(e,o,a){const i=N(),n=B.has(a)?S(e,o):null,{changedAt:t}=K(n);f.useEffect(()=>{t!==null&&(i.invalidateQueries({queryKey:l.fileRead(e)}),i.invalidateQueries({queryKey:l.fileDownload(e)}),i.invalidateQueries({queryKey:l.fileDiffByPath(e)}))},[t,e,i])}function y(e){return"path"in e}function F(e){return"staged"in e}function M({tab:e}){var p;const o=e.kind==="diff",a=e.data,i=y(a)?a.path:"",n=y(a)&&F(a)?a.staged:!1,[t,d]=f.useState(o?"diff":"file"),u=e.workspaceCwd||"~",m=w(i);C(i,u,m);const c=q(i,u,n,t==="diff"),v=E[m];return s.jsxs("div",{className:r.panel,children:[s.jsxs("div",{className:r.header,children:[s.jsx("span",{className:r.path,children:i}),s.jsx("span",{className:r.size,children:t==="diff"?n?"staged":"unstaged":null}),s.jsxs("div",{className:r.viewToggle,children:[s.jsx(h,{size:"xs",variant:t==="file"?"default":"ghost",onClick:()=>d("file"),children:"File"}),s.jsx(h,{size:"xs",variant:t==="diff"?"default":"ghost",onClick:()=>d("diff"),children:"Diff"})]})]}),s.jsx("div",{className:j(r.body,"file-body"),children:t==="file"?s.jsx(f.Suspense,{fallback:s.jsxs("div",{className:r.emptyState,children:[s.jsx(x,{size:"sm"})," Loading..."]}),children:s.jsx(v,{path:i})}):c.isLoading?s.jsxs("div",{className:r.emptyState,children:[s.jsx(x,{size:"sm"})," Loading diff..."]}):c.isError?s.jsxs("div",{className:r.emptyState,children:["Error: ",c.error instanceof Error?c.error.message:"unknown error"]}):(p=c.data)!=null&&p.patch?s.jsx(k,{patch:c.data.patch}):s.jsx("div",{className:r.diffEmpty,children:"No changes (file matches HEAD)"})})]})}export{M as FilePanel};
2
+ //# sourceMappingURL=FilePanel-BrbOLTdf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilePanel-BrbOLTdf.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 { 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 <div className={styles.panel}>\n <div className={styles.header}>\n <span className={styles.path}>{path}</span>\n <span className={styles.size}>{viewMode === \"diff\" ? (staged ? \"staged\" : \"unstaged\") : null}</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 fallback={<div className={styles.emptyState}><Spinner size=\"sm\" /> Loading...</div>}>\n <Viewer path={path} />\n </Suspense>\n ) : diffQuery.isLoading ? (\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Loading diff...\n </div>\n ) : diffQuery.isError ? (\n <div className={styles.emptyState}>\n Error: {diffQuery.error instanceof Error ? diffQuery.error.message : \"unknown error\"}\n </div>\n ) : diffQuery.data?.patch ? (\n <DiffView patch={diffQuery.data.patch} />\n ) : (\n <div className={styles.diffEmpty}>No changes (file matches HEAD)</div>\n )}\n </div>\n </div>\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","jsxs","styles","jsx","Button","clsx","Suspense","Spinner","_a","DiffView"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{j as e}from"./vendor-react-p-LLqNcx.js";import{S as a}from"./index-DneBC_ta.js";import{u as t}from"./use-file-download-DsIqcxhB.js";import{p as s}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";const m="_wrap_1al28_1",n="_image_1al28_14",o={wrap:m,image:n};function y({path:i}){const r=t(i);return r.isLoading?e.jsxs("div",{className:s.emptyState,children:[e.jsx(a,{size:"sm"})," Loading image..."]}):r.isError?e.jsxs("div",{className:s.emptyState,children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?e.jsx("div",{className:o.wrap,children:e.jsx("img",{className:o.image,src:r.data.objectUrl,alt:i.split("/").pop()||i})}):null}export{y as ImageViewer};
2
- //# sourceMappingURL=ImageViewer-BKflylHn.js.map
1
+ import{j as e}from"./vendor-react-p-LLqNcx.js";import{S as a}from"./index-DulchPgH.js";import{u as t}from"./use-file-download-AaY3793H.js";import{p as s}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";const m="_wrap_1al28_1",n="_image_1al28_14",o={wrap:m,image:n};function y({path:i}){const r=t(i);return r.isLoading?e.jsxs("div",{className:s.emptyState,children:[e.jsx(a,{size:"sm"})," Loading image..."]}):r.isError?e.jsxs("div",{className:s.emptyState,children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?e.jsx("div",{className:o.wrap,children:e.jsx("img",{className:o.image,src:r.data.objectUrl,alt:i.split("/").pop()||i})}):null}export{y as ImageViewer};
2
+ //# sourceMappingURL=ImageViewer-B82Lth8R.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImageViewer-BKflylHn.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 { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport panelStyles from \"./FilePanel.module.css\";\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 <div className={panelStyles.emptyState}>\n <Spinner size=\"sm\" /> Loading image...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={panelStyles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n <img className={styles.image} src={query.data.objectUrl} alt={path.split(\"/\").pop() || path} />\n </div>\n );\n}\n"],"names":["ImageViewer","path","query","useFileDownload","jsxs","panelStyles","jsx","Spinner","styles"],"mappings":"ofAYO,SAASA,EAAY,CAAE,KAAAC,GAAqB,CACjD,MAAMC,EAAQC,EAAgBF,CAAI,EAElC,OAAIC,EAAM,UAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAC1B,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,mBAAA,EACvB,EAGAL,EAAM,QAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAAY,SAAA,CAAA,UAC9BH,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,KAGTI,MAAC,OAAI,UAAWE,EAAO,KACrB,SAAAF,MAAC,MAAA,CAAI,UAAWE,EAAO,MAAO,IAAKN,EAAM,KAAK,UAAW,IAAKD,EAAK,MAAM,GAAG,EAAE,IAAA,GAASA,CAAA,CAAM,CAAA,CAC/F,EALsB,IAO1B"}
1
+ {"version":3,"file":"ImageViewer-B82Lth8R.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 { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport panelStyles from \"./FilePanel.module.css\";\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 <div className={panelStyles.emptyState}>\n <Spinner size=\"sm\" /> Loading image...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={panelStyles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n <img className={styles.image} src={query.data.objectUrl} alt={path.split(\"/\").pop() || path} />\n </div>\n );\n}\n"],"names":["ImageViewer","path","query","useFileDownload","jsxs","panelStyles","jsx","Spinner","styles"],"mappings":"ofAYO,SAASA,EAAY,CAAE,KAAAC,GAAqB,CACjD,MAAMC,EAAQC,EAAgBF,CAAI,EAElC,OAAIC,EAAM,UAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAC1B,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,mBAAA,EACvB,EAGAL,EAAM,QAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAAY,SAAA,CAAA,UAC9BH,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,KAGTI,MAAC,OAAI,UAAWE,EAAO,KACrB,SAAAF,MAAC,MAAA,CAAI,UAAWE,EAAO,MAAO,IAAKN,EAAM,KAAK,UAAW,IAAKD,EAAK,MAAM,GAAG,EAAE,IAAA,GAASA,CAAA,CAAM,CAAA,CAC/F,EALsB,IAO1B"}
@@ -0,0 +1,2 @@
1
+ import{r as d,j as r}from"./vendor-react-p-LLqNcx.js";import{S as c,B as a}from"./index-DulchPgH.js";import{u as m,C as p}from"./CodeView-DRJLXH-w.js";import{M as l}from"./markdown-C6wNVyCO.js";import{p as n}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";import"./paths--dEDMUm1.js";const x="_wrap_khhx0_1",u="_toolbar_khhx0_6",h="_rendered_khhx0_17",o={wrap:x,toolbar:u,rendered:h};function M({path:s}){const e=m(s),[t,i]=d.useState("preview");return e.isLoading?r.jsxs("div",{className:n.emptyState,children:[r.jsx(c,{size:"sm"})," Loading..."]}):e.isError?r.jsxs("div",{className:n.emptyState,children:["Error: ",e.error instanceof Error?e.error.message:"unknown error"]}):e.data?r.jsxs("div",{className:o.wrap,children:[r.jsxs("div",{className:o.toolbar,children:[r.jsx(a,{size:"xs",variant:t==="preview"?"default":"ghost",onClick:()=>i("preview"),children:"Preview"}),r.jsx(a,{size:"xs",variant:t==="source"?"default":"ghost",onClick:()=>i("source"),children:"Source"})]}),t==="preview"?r.jsx("div",{className:o.rendered,children:r.jsx(l,{text:e.data.content})}):r.jsx(p,{path:s,content:e.data.content})]}):null}export{M as MarkdownFileViewer};
2
+ //# sourceMappingURL=MarkdownFileViewer-uPA7NmW6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownFileViewer-BDmo3la-.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 { 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 panelStyles from \"./FilePanel.module.css\";\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 <div className={panelStyles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={panelStyles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n <div className={styles.toolbar}>\n <Button size=\"xs\" variant={mode === \"preview\" ? \"default\" : \"ghost\"} onClick={() => setMode(\"preview\")}>\n Preview\n </Button>\n <Button size=\"xs\" variant={mode === \"source\" ? \"default\" : \"ghost\"} onClick={() => setMode(\"source\")}>\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","jsxs","panelStyles","jsx","Spinner","styles","Button","Markdown","CodeView"],"mappings":"glBAoBO,SAASA,EAAmB,CAAE,KAAAC,GAAqB,CACxD,MAAMC,EAAQC,EAAYF,CAAI,EACxB,CAACG,EAAMC,CAAO,EAAIC,EAAAA,SAAe,SAAS,EAEhD,OAAIJ,EAAM,UAENK,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAC1B,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,EACvB,EAGAR,EAAM,QAENK,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAAY,SAAA,CAAA,UAC9BN,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,KAGTK,EAAAA,KAAC,MAAA,CAAI,UAAWI,EAAO,KACrB,SAAA,CAAAJ,EAAAA,KAAC,MAAA,CAAI,UAAWI,EAAO,QACrB,SAAA,CAAAF,EAAAA,IAACG,EAAA,CAAO,KAAK,KAAK,QAASR,IAAS,UAAY,UAAY,QAAS,QAAS,IAAMC,EAAQ,SAAS,EAAG,SAAA,UAExG,EACAI,EAAAA,IAACG,EAAA,CAAO,KAAK,KAAK,QAASR,IAAS,SAAW,UAAY,QAAS,QAAS,IAAMC,EAAQ,QAAQ,EAAG,SAAA,QAAA,CAEtG,CAAA,EACF,EACCD,IAAS,UACRK,MAAC,MAAA,CAAI,UAAWE,EAAO,SACrB,eAACE,EAAA,CAAS,KAAMX,EAAM,KAAK,OAAA,CAAS,EACtC,EAEAO,EAAAA,IAACK,GAAS,KAAAb,EAAY,QAASC,EAAM,KAAK,OAAA,CAAS,CAAA,EAEvD,EAnBsB,IAqB1B"}
1
+ {"version":3,"file":"MarkdownFileViewer-uPA7NmW6.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 { 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 panelStyles from \"./FilePanel.module.css\";\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 <div className={panelStyles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={panelStyles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n <div className={styles.toolbar}>\n <Button size=\"xs\" variant={mode === \"preview\" ? \"default\" : \"ghost\"} onClick={() => setMode(\"preview\")}>\n Preview\n </Button>\n <Button size=\"xs\" variant={mode === \"source\" ? \"default\" : \"ghost\"} onClick={() => setMode(\"source\")}>\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","jsxs","panelStyles","jsx","Spinner","styles","Button","Markdown","CodeView"],"mappings":"4mBAoBO,SAASA,EAAmB,CAAE,KAAAC,GAAqB,CACxD,MAAMC,EAAQC,EAAYF,CAAI,EACxB,CAACG,EAAMC,CAAO,EAAIC,EAAAA,SAAe,SAAS,EAEhD,OAAIJ,EAAM,UAENK,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAC1B,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,EACvB,EAGAR,EAAM,QAENK,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAAY,SAAA,CAAA,UAC9BN,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,KAGTK,EAAAA,KAAC,MAAA,CAAI,UAAWI,EAAO,KACrB,SAAA,CAAAJ,EAAAA,KAAC,MAAA,CAAI,UAAWI,EAAO,QACrB,SAAA,CAAAF,EAAAA,IAACG,EAAA,CAAO,KAAK,KAAK,QAASR,IAAS,UAAY,UAAY,QAAS,QAAS,IAAMC,EAAQ,SAAS,EAAG,SAAA,UAExG,EACAI,EAAAA,IAACG,EAAA,CAAO,KAAK,KAAK,QAASR,IAAS,SAAW,UAAY,QAAS,QAAS,IAAMC,EAAQ,QAAQ,EAAG,SAAA,QAAA,CAEtG,CAAA,EACF,EACCD,IAAS,UACRK,MAAC,MAAA,CAAI,UAAWE,EAAO,SACrB,eAACE,EAAA,CAAS,KAAMX,EAAM,KAAK,OAAA,CAAS,EACtC,EAEAO,EAAAA,IAACK,GAAS,KAAAb,EAAY,QAASC,EAAM,KAAK,OAAA,CAAS,CAAA,EAEvD,EAnBsB,IAqB1B"}
@@ -1,2 +1,2 @@
1
- import{r as c,j as e}from"./vendor-react-p-LLqNcx.js";import{S as g,c as h}from"./index-DneBC_ta.js";import{i as E}from"./vendor-molviewer-PwwY6jQU.js";import{S}from"./StatusBadge-D5M084rB.js";import{u as b}from"./use-file-download-DsIqcxhB.js";import{u as y}from"./use-file-watch-ld_2lbTl.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";function R(r,s){return!r||!s?null:{url:s,name:r.split("/").pop()||r}}function N({changedAt:r,lastAppliedAt:s,modified:l}){return!(r===null||l||r===s)}const V={"--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-surfaceHover)","--control-strong-hover":"var(--pi-color-surfaceHover)","--border-control":"var(--pi-color-border)","--border-input":"var(--pi-color-border)","--popover":"var(--pi-color-surface1)","--popover-item-hover":"var(--pi-color-surfaceHover)","--row-hover":"var(--pi-color-surfaceHover)","--border-popover":"var(--pi-color-border)","--text-primary":"var(--pi-color-foreground)","--text-on-control":"var(--pi-color-foreground)"},A="_wrap_auscj_1",B="_molViewer_auscj_8",L="_emptyState_auscj_12",M="_staleBadge_auscj_21",t={wrap:A,molViewer:B,emptyState:L,staleBadge:M};function H({path:r,isActive:s,onModifiedChange:l}){var v;const o=b(r??"",!!r),{changedAt:a}=y(r),w=c.useRef(null),d=c.useRef(!1),[i,f]=c.useState(null),[u,j]=c.useState(!1),p=c.useRef(null),x=R(r,(v=o.data)==null?void 0:v.objectUrl);c.useEffect(()=>{N({changedAt:a,lastAppliedAt:p.current,modified:u})&&(p.current=a,o.refetch())},[a,u,o.refetch]);const _=a!==null&&a!==p.current&&u,m=(o.isError?o.error instanceof Error?o.error.message:"download failed":null)??(i==null?void 0:i.message)??null;return r&&o.isPending?e.jsx("div",{className:t.wrap,children:e.jsxs("div",{className:t.emptyState,children:[e.jsx(g,{size:"sm"})," Loading..."]})}):m?e.jsx("div",{className:t.wrap,children:e.jsxs("div",{className:t.emptyState,children:["Error: ",m]})}):e.jsxs("div",{className:t.wrap,"data-molecule-active":s?"true":"false",children:[_&&e.jsx(S,{label:"File changed on disk",variant:"muted",className:t.staleBadge}),e.jsx(E,{ref:w,className:t.molViewer,theme:V,source:x,sourceMode:d.current?"update":"replace",onLoad:()=>{d.current=!0,f(null)},onLoadError:n=>f(n.error),onModifiedChange:n=>{j(n),l==null||l(n)}})]})}const T="_panel_15zlw_1",I={panel:T};function Q({tab:r}){const{path:s}=r.data,o=h(a=>a.activeTabId)===r.id;return e.jsx("div",{className:I.panel,children:e.jsx(H,{path:s,isActive:o})})}export{Q as MoleculeViewerPanel};
2
- //# sourceMappingURL=MoleculeViewerPanel-BZA00vSx.js.map
1
+ import{r as c,j as e}from"./vendor-react-p-LLqNcx.js";import{S as g,g as h}from"./index-DulchPgH.js";import{i as E}from"./vendor-molviewer-PwwY6jQU.js";import{S}from"./StatusBadge-D5M084rB.js";import{u as b}from"./use-file-download-AaY3793H.js";import{u as y}from"./use-file-watch-C52FKLSq.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";function R(r,s){return!r||!s?null:{url:s,name:r.split("/").pop()||r}}function N({changedAt:r,lastAppliedAt:s,modified:l}){return!(r===null||l||r===s)}const V={"--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-surfaceHover)","--control-strong-hover":"var(--pi-color-surfaceHover)","--border-control":"var(--pi-color-border)","--border-input":"var(--pi-color-border)","--popover":"var(--pi-color-surface1)","--popover-item-hover":"var(--pi-color-surfaceHover)","--row-hover":"var(--pi-color-surfaceHover)","--border-popover":"var(--pi-color-border)","--text-primary":"var(--pi-color-foreground)","--text-on-control":"var(--pi-color-foreground)"},A="_wrap_auscj_1",B="_molViewer_auscj_8",L="_emptyState_auscj_12",M="_staleBadge_auscj_21",t={wrap:A,molViewer:B,emptyState:L,staleBadge:M};function H({path:r,isActive:s,onModifiedChange:l}){var v;const o=b(r??"",!!r),{changedAt:a}=y(r),w=c.useRef(null),d=c.useRef(!1),[i,f]=c.useState(null),[u,j]=c.useState(!1),p=c.useRef(null),x=R(r,(v=o.data)==null?void 0:v.objectUrl);c.useEffect(()=>{N({changedAt:a,lastAppliedAt:p.current,modified:u})&&(p.current=a,o.refetch())},[a,u,o.refetch]);const _=a!==null&&a!==p.current&&u,m=(o.isError?o.error instanceof Error?o.error.message:"download failed":null)??(i==null?void 0:i.message)??null;return r&&o.isPending?e.jsx("div",{className:t.wrap,children:e.jsxs("div",{className:t.emptyState,children:[e.jsx(g,{size:"sm"})," Loading..."]})}):m?e.jsx("div",{className:t.wrap,children:e.jsxs("div",{className:t.emptyState,children:["Error: ",m]})}):e.jsxs("div",{className:t.wrap,"data-molecule-active":s?"true":"false",children:[_&&e.jsx(S,{label:"File changed on disk",variant:"muted",className:t.staleBadge}),e.jsx(E,{ref:w,className:t.molViewer,theme:V,source:x,sourceMode:d.current?"update":"replace",onLoad:()=>{d.current=!0,f(null)},onLoadError:n=>f(n.error),onModifiedChange:n=>{j(n),l==null||l(n)}})]})}const T="_panel_15zlw_1",I={panel:T};function Q({tab:r}){const{path:s}=r.data,o=h(a=>a.activeTabId)===r.id;return e.jsx("div",{className:I.panel,children:e.jsx(H,{path:s,isActive:o})})}export{Q as MoleculeViewerPanel};
2
+ //# sourceMappingURL=MoleculeViewerPanel-Ct9B4hGp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MoleculeViewerPanel-BZA00vSx.js","sources":["../../../src/features/files/molecule-source.ts","../../../src/features/files/molecule-reload.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 * 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-surfaceHover)\",\n \"--control-strong-hover\": \"var(--pi-color-surfaceHover)\",\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-surfaceHover)\",\n \"--row-hover\": \"var(--pi-color-surfaceHover)\",\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\nimport { useEffect, useRef, useState } from \"react\";\nimport { MolViewer, type MolViewerHandle } from \"@molviewer/core\";\nimport \"@molviewer/core/style.css\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { StatusBadge } from \"@pi-studio-ui/components/primitives/StatusBadge.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { useFileWatch } from \"@pi-studio-ui/hooks/use-file-watch.js\";\nimport { moleculeSource } from \"./molecule-source.js\";\nimport { shouldApplyRefresh } from \"./molecule-reload.js\";\nimport { MOLVIEWER_THEME } from \"./molecule-theme.js\";\nimport styles from \"./MoleculeViewer.module.css\";\n\nexport interface MoleculeViewerProps {\n /** Absolute path of the file to render, or null for an empty (\"New molecule view\") tab. */\n path: string | null;\n /** True when this viewer's tab is the visible one. Molstar's own `ResizeObserver` on its canvas\n * container re-fits on becoming visible (verified in the installed bundle), so this is not\n * currently used to drive a manual re-fit — kept as the escape hatch task-010's visual\n * verification may still need. */\n isActive?: boolean;\n /** Mirrors `MolViewerProps.onModifiedChange` upward for a future parent-level consumer (e.g. a\n * tab-strip \"unsaved changes\" mark) — independent of this component's own internal `modified`\n * state below, which drives the live-reload gate. */\n onModifiedChange?: (modified: boolean) => void;\n}\n\nexport function MoleculeViewer({ path, isActive, onModifiedChange }: MoleculeViewerProps) {\n const 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 // Ref, not state: recording \"which push we last acted on\" doesn't need its own render — it's\n // read by the same effect that writes it, and by the stale-indicator check below.\n const lastAppliedAtRef = useRef<number | null>(null);\n\n const source = moleculeSource(path, download.data?.objectUrl);\n\n useEffect(() => {\n if (!shouldApplyRefresh({ changedAt, lastAppliedAt: lastAppliedAtRef.current, modified })) {\n return;\n }\n lastAppliedAtRef.current = changedAt;\n void download.refetch();\n // Only `download.refetch` itself needs to be stable across renders for this effect to behave\n // correctly; depending on the whole `download` query object would re-run on every unrelated\n // status change it produces.\n }, [changedAt, modified, download.refetch]);\n\n // A push arrived but couldn't be applied because of unsaved edits — surface that rather than\n // silently diverging. Reading the ref during render is safe here: it only ever changes inside\n // the effect above, which always runs before the next paint that could observe a stale value.\n const hasUnappliedChange =\n changedAt !== null && changedAt !== lastAppliedAtRef.current && modified;\n\n 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 <div className={styles.wrap}>\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n </div>\n );\n }\n if (errorMessage) {\n return (\n <div className={styles.wrap}>\n <div className={styles.emptyState}>Error: {errorMessage}</div>\n </div>\n );\n }\n\n return (\n <div className={styles.wrap} data-molecule-active={isActive ? \"true\" : \"false\"}>\n {hasUnappliedChange && (\n <StatusBadge label=\"File changed on disk\" variant=\"muted\" className={styles.staleBadge} />\n )}\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 />\n </div>\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 { useTabStore, type MoleculeTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { PanelProps } from \"@pi-studio-ui/features/workspace/panel-registry.js\";\nimport { MoleculeViewer } from \"./MoleculeViewer.js\";\nimport styles from \"./MoleculeViewerPanel.module.css\";\n\nexport function MoleculeViewerPanel({ tab }: PanelProps) {\n const { path } = tab.data as MoleculeTabData;\n const activeTabId = useTabStore((s) => s.activeTabId);\n const isActive = activeTabId === tab.id;\n\n return (\n <div className={styles.panel}>\n <MoleculeViewer path={path} isActive={isActive} />\n </div>\n );\n}\n"],"names":["moleculeSource","path","objectUrl","shouldApplyRefresh","changedAt","lastAppliedAt","modified","MOLVIEWER_THEME","MoleculeViewer","isActive","onModifiedChange","download","useFileDownload","useFileWatch","handleRef","useRef","hasLoadedRef","loadError","setLoadError","useState","setModified","lastAppliedAtRef","source","_a","useEffect","hasUnappliedChange","errorMessage","jsx","styles","jsxs","Spinner","StatusBadge","MolViewer","e","m","MoleculeViewerPanel","tab","useTabStore","s"],"mappings":"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"}
1
+ {"version":3,"file":"MoleculeViewerPanel-Ct9B4hGp.js","sources":["../../../src/features/files/molecule-source.ts","../../../src/features/files/molecule-reload.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 * 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-surfaceHover)\",\n \"--control-strong-hover\": \"var(--pi-color-surfaceHover)\",\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-surfaceHover)\",\n \"--row-hover\": \"var(--pi-color-surfaceHover)\",\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\nimport { useEffect, useRef, useState } from \"react\";\nimport { MolViewer, type MolViewerHandle } from \"@molviewer/core\";\nimport \"@molviewer/core/style.css\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { StatusBadge } from \"@pi-studio-ui/components/primitives/StatusBadge.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { useFileWatch } from \"@pi-studio-ui/hooks/use-file-watch.js\";\nimport { moleculeSource } from \"./molecule-source.js\";\nimport { shouldApplyRefresh } from \"./molecule-reload.js\";\nimport { MOLVIEWER_THEME } from \"./molecule-theme.js\";\nimport styles from \"./MoleculeViewer.module.css\";\n\nexport interface MoleculeViewerProps {\n /** Absolute path of the file to render, or null for an empty (\"New molecule view\") tab. */\n path: string | null;\n /** True when this viewer's tab is the visible one. Molstar's own `ResizeObserver` on its canvas\n * container re-fits on becoming visible (verified in the installed bundle), so this is not\n * currently used to drive a manual re-fit — kept as the escape hatch task-010's visual\n * verification may still need. */\n isActive?: boolean;\n /** Mirrors `MolViewerProps.onModifiedChange` upward for a future parent-level consumer (e.g. a\n * tab-strip \"unsaved changes\" mark) — independent of this component's own internal `modified`\n * state below, which drives the live-reload gate. */\n onModifiedChange?: (modified: boolean) => void;\n}\n\nexport function MoleculeViewer({ path, isActive, onModifiedChange }: MoleculeViewerProps) {\n const 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 // Ref, not state: recording \"which push we last acted on\" doesn't need its own render — it's\n // read by the same effect that writes it, and by the stale-indicator check below.\n const lastAppliedAtRef = useRef<number | null>(null);\n\n const source = moleculeSource(path, download.data?.objectUrl);\n\n useEffect(() => {\n if (!shouldApplyRefresh({ changedAt, lastAppliedAt: lastAppliedAtRef.current, modified })) {\n return;\n }\n lastAppliedAtRef.current = changedAt;\n void download.refetch();\n // Only `download.refetch` itself needs to be stable across renders for this effect to behave\n // correctly; depending on the whole `download` query object would re-run on every unrelated\n // status change it produces.\n }, [changedAt, modified, download.refetch]);\n\n // A push arrived but couldn't be applied because of unsaved edits — surface that rather than\n // silently diverging. Reading the ref during render is safe here: it only ever changes inside\n // the effect above, which always runs before the next paint that could observe a stale value.\n const hasUnappliedChange =\n changedAt !== null && changedAt !== lastAppliedAtRef.current && modified;\n\n 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 <div className={styles.wrap}>\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n </div>\n );\n }\n if (errorMessage) {\n return (\n <div className={styles.wrap}>\n <div className={styles.emptyState}>Error: {errorMessage}</div>\n </div>\n );\n }\n\n return (\n <div className={styles.wrap} data-molecule-active={isActive ? \"true\" : \"false\"}>\n {hasUnappliedChange && (\n <StatusBadge label=\"File changed on disk\" variant=\"muted\" className={styles.staleBadge} />\n )}\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 />\n </div>\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 { useTabStore, type MoleculeTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { PanelProps } from \"@pi-studio-ui/features/workspace/panel-registry.js\";\nimport { MoleculeViewer } from \"./MoleculeViewer.js\";\nimport styles from \"./MoleculeViewerPanel.module.css\";\n\nexport function MoleculeViewerPanel({ tab }: PanelProps) {\n const { path } = tab.data as MoleculeTabData;\n const activeTabId = useTabStore((s) => s.activeTabId);\n const isActive = activeTabId === tab.id;\n\n return (\n <div className={styles.panel}>\n <MoleculeViewer path={path} isActive={isActive} />\n </div>\n );\n}\n"],"names":["moleculeSource","path","objectUrl","shouldApplyRefresh","changedAt","lastAppliedAt","modified","MOLVIEWER_THEME","MoleculeViewer","isActive","onModifiedChange","download","useFileDownload","useFileWatch","handleRef","useRef","hasLoadedRef","loadError","setLoadError","useState","setModified","lastAppliedAtRef","source","_a","useEffect","hasUnappliedChange","errorMessage","jsx","styles","jsxs","Spinner","StatusBadge","MolViewer","e","m","MoleculeViewerPanel","tab","useTabStore","s"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- var A=Object.defineProperty;var I=(n,e,t)=>e in n?A(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var g=(n,e,t)=>I(n,typeof e!="symbol"?e+"":e,t);import{r as a,j as l}from"./vendor-react-p-LLqNcx.js";import{x as B,a as F}from"./vendor-terminal-BhzX2_ts.js";import{b as E,a as N,c as T,S as D}from"./index-DneBC_ta.js";import"./vendor-B2Vnrid8.js";import"./vendor-query-chxBrUuj.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-markdown-ePgMI45n.js";class O{constructor(e){g(this,"subscribers",new Map);g(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(E({opcode:"Input",slot:e,data:t}))}sendResize(e,t,m){this.daemon.sendBinary(E({opcode:"Resize",slot:e,rows:t,cols:m}))}dispatch(e){var m,p,u;const t=this.subscribers.get(e.slot);if(t)switch(e.opcode){case"Output":(m=t.onOutput)==null||m.call(t,e.data);return;case"Snapshot":(p=t.onSnapshot)==null||p.call(t,e.data);return;case"Restore":(u=t.onRestore)==null||u.call(t,e.data);return;default:return}}}const C="_wrap_b74ul_1",P="_terminal_b74ul_11",W="_status_b74ul_18",f={wrap:C,terminal:P,status:W},q=new WeakMap;function G(n){let e=q.get(n);return e||(e=new O(n),e.start(),q.set(n,e)),e}const H={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:n}){const e=n.data,t=N(r=>r.client),p=T(r=>r.activeTabId)===n.id,u=a.useRef(null),w=a.useRef(null),h=a.useRef(null),b=a.useRef(e.slot),S=a.useRef(t);S.current=t;const[c,k]=a.useState(e.slot),[_,v]=a.useState(null),d=a.useRef(!1),R=a.useRef(!1);return a.useEffect(()=>{if(d.current=!0,!t||b.current!==null||R.current)return()=>{d.current=!1};R.current=!0;const r=e.cwd||"~";return t.connection.request("create_terminal_request",{workspaceId:"",cwd:r}).then(s=>{const i=s.terminal.slot;if(!d.current){t.connection.request("kill_terminal_request",{slot:i}).catch(()=>{});return}b.current=i,k(i),T.getState().updateData(n.id,{slot:i})}).catch(s=>{d.current&&v(s instanceof Error?s.message:String(s))}).finally(()=>{R.current=!1}),()=>{d.current=!1}},[t,n.id]),a.useEffect(()=>()=>{const r=b.current;r!==null&&setTimeout(()=>{var s;d.current||(s=S.current)==null||s.connection.request("kill_terminal_request",{slot:r}).catch(()=>{})},0)},[]),a.useEffect(()=>{if(!t||c===null||!u.current)return;const r=new B.Terminal({cursorBlink:!0,fontFamily:"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",fontSize:13,scrollback:5e3,theme:H,allowProposedApi:!0}),s=new F.FitAddon;r.loadAddon(s),r.open(u.current),s.fit(),w.current=r,h.current=s;const i=G(t.connection),j=i.subscribeSlot(c,{onOutput:o=>r.write(o),onSnapshot:o=>{r.clear(),r.write(o)},onRestore:o=>r.write(o)});t.connection.request("subscribe_terminal_request",{slot:c}).catch(o=>{v(o instanceof Error?o.message:String(o))});const y=r.onData(o=>{i.sendInput(c,new TextEncoder().encode(o))}),z=r.onResize(({cols:o,rows:M})=>{i.sendResize(c,M,o)}),x=new ResizeObserver(()=>{s.fit()});return x.observe(u.current),()=>{x.disconnect(),y.dispose(),z.dispose(),j(),t.connection.request("unsubscribe_terminal_request",{slot:c}).catch(()=>{}),r.dispose(),w.current=null,h.current=null}},[t,c]),a.useEffect(()=>{var r;p&&((r=h.current)==null||r.fit())},[p]),_?l.jsx("div",{className:f.wrap,children:l.jsxs("div",{className:f.status,children:["Terminal error: ",_]})}):c===null?l.jsx("div",{className:f.wrap,children:l.jsxs("div",{className:f.status,children:[l.jsx(D,{size:"sm"})," Starting terminal…"]})}):l.jsx("div",{className:f.wrap,children:l.jsx("div",{ref:u,className:f.terminal})})}export{te as TerminalPanel};
2
- //# sourceMappingURL=TerminalPanel-qx_HdM7y.js.map
1
+ var A=Object.defineProperty;var I=(n,e,t)=>e in n?A(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var g=(n,e,t)=>I(n,typeof e!="symbol"?e+"":e,t);import{r as a,j as l}from"./vendor-react-p-LLqNcx.js";import{x as B,a as F}from"./vendor-terminal-BhzX2_ts.js";import{c as E,a as N,g as T,S as D}from"./index-DulchPgH.js";import"./vendor-B2Vnrid8.js";import"./vendor-query-chxBrUuj.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-markdown-ePgMI45n.js";class O{constructor(e){g(this,"subscribers",new Map);g(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(E({opcode:"Input",slot:e,data:t}))}sendResize(e,t,m){this.daemon.sendBinary(E({opcode:"Resize",slot:e,rows:t,cols:m}))}dispatch(e){var m,p,u;const t=this.subscribers.get(e.slot);if(t)switch(e.opcode){case"Output":(m=t.onOutput)==null||m.call(t,e.data);return;case"Snapshot":(p=t.onSnapshot)==null||p.call(t,e.data);return;case"Restore":(u=t.onRestore)==null||u.call(t,e.data);return;default:return}}}const C="_wrap_b74ul_1",P="_terminal_b74ul_11",W="_status_b74ul_18",f={wrap:C,terminal:P,status:W},q=new WeakMap;function G(n){let e=q.get(n);return e||(e=new O(n),e.start(),q.set(n,e)),e}const H={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:n}){const e=n.data,t=N(r=>r.client),p=T(r=>r.activeTabId)===n.id,u=a.useRef(null),w=a.useRef(null),h=a.useRef(null),b=a.useRef(e.slot),S=a.useRef(t);S.current=t;const[c,k]=a.useState(e.slot),[_,v]=a.useState(null),d=a.useRef(!1),R=a.useRef(!1);return a.useEffect(()=>{if(d.current=!0,!t||b.current!==null||R.current)return()=>{d.current=!1};R.current=!0;const r=e.cwd||"~";return t.connection.request("create_terminal_request",{workspaceId:"",cwd:r}).then(s=>{const i=s.terminal.slot;if(!d.current){t.connection.request("kill_terminal_request",{slot:i}).catch(()=>{});return}b.current=i,k(i),T.getState().updateData(n.id,{slot:i})}).catch(s=>{d.current&&v(s instanceof Error?s.message:String(s))}).finally(()=>{R.current=!1}),()=>{d.current=!1}},[t,n.id]),a.useEffect(()=>()=>{const r=b.current;r!==null&&setTimeout(()=>{var s;d.current||(s=S.current)==null||s.connection.request("kill_terminal_request",{slot:r}).catch(()=>{})},0)},[]),a.useEffect(()=>{if(!t||c===null||!u.current)return;const r=new B.Terminal({cursorBlink:!0,fontFamily:"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",fontSize:13,scrollback:5e3,theme:H,allowProposedApi:!0}),s=new F.FitAddon;r.loadAddon(s),r.open(u.current),s.fit(),w.current=r,h.current=s;const i=G(t.connection),j=i.subscribeSlot(c,{onOutput:o=>r.write(o),onSnapshot:o=>{r.clear(),r.write(o)},onRestore:o=>r.write(o)});t.connection.request("subscribe_terminal_request",{slot:c}).catch(o=>{v(o instanceof Error?o.message:String(o))});const y=r.onData(o=>{i.sendInput(c,new TextEncoder().encode(o))}),z=r.onResize(({cols:o,rows:M})=>{i.sendResize(c,M,o)}),x=new ResizeObserver(()=>{s.fit()});return x.observe(u.current),()=>{x.disconnect(),y.dispose(),z.dispose(),j(),t.connection.request("unsubscribe_terminal_request",{slot:c}).catch(()=>{}),r.dispose(),w.current=null,h.current=null}},[t,c]),a.useEffect(()=>{var r;p&&((r=h.current)==null||r.fit())},[p]),_?l.jsx("div",{className:f.wrap,children:l.jsxs("div",{className:f.status,children:["Terminal error: ",_]})}):c===null?l.jsx("div",{className:f.wrap,children:l.jsxs("div",{className:f.status,children:[l.jsx(D,{size:"sm"})," Starting terminal…"]})}):l.jsx("div",{className:f.wrap,children:l.jsx("div",{ref:u,className:f.terminal})})}export{te as TerminalPanel};
2
+ //# sourceMappingURL=TerminalPanel-CXsU1Xtg.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TerminalPanel-qx_HdM7y.js","sources":["../../../../client/src/terminal-stream-router.ts","../../../src/features/terminal/TerminalPanel.tsx"],"sourcesContent":["import { encodeTerminalFrame, type TerminalFrame } from \"@av-pi-studio/protocol\";\n\nimport type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Client-side demux of binary terminal frames to per-slot subscribers, plus outbound input/resize\n * encoding (architecture/client-app-runtime.md § Router; features/terminals.md § Binary stream\n * protocol).\n *\n * Inbound `Output`/`Snapshot`/`Restore` frames are dispatched to the subscriber registered for that\n * `slot`. Outbound `Input`/`Resize` are encoded with the right opcode + slot and sent on the data\n * path.\n */\n\nexport interface TerminalSlotSubscriber {\n /** Live terminal output bytes. */\n onOutput?: (data: Uint8Array) => void;\n /** Full-screen snapshot bytes (sent on (re)subscribe). */\n onSnapshot?: (data: Uint8Array) => void;\n /** Restore snapshot bytes (reflowable/mode-gated). */\n onRestore?: (data: Uint8Array) => void;\n}\n\nexport class TerminalStreamRouter {\n private readonly subscribers = new Map<number, TerminalSlotSubscriber>();\n private detach: (() => void) | null = null;\n\n constructor(private readonly daemon: DaemonClient) {}\n\n /** Begin routing inbound terminal frames. Idempotent. */\n start(): void {\n if (this.detach) return;\n this.detach = this.daemon.onTerminalFrame((frame) => this.dispatch(frame));\n }\n\n /** Stop routing inbound frames (subscribers retained). */\n stop(): void {\n this.detach?.();\n this.detach = null;\n }\n\n /** Register (or replace) the subscriber for a slot. Returns an unsubscribe fn. */\n subscribeSlot(slot: number, subscriber: TerminalSlotSubscriber): () => void {\n this.subscribers.set(slot, subscriber);\n return () => {\n if (this.subscribers.get(slot) === subscriber) this.subscribers.delete(slot);\n };\n }\n\n /** True iff a subscriber is registered for the slot. */\n hasSlot(slot: number): boolean {\n return this.subscribers.has(slot);\n }\n\n // ─── Outbound ─────────────────────────────────────────────────────────────\n\n /** Send raw input bytes to a slot's PTY (opcode `Input = 0x02`). */\n sendInput(slot: number, data: Uint8Array): void {\n this.daemon.sendBinary(encodeTerminalFrame({ opcode: \"Input\", slot, data }));\n }\n\n /** Send a resize (opcode `Resize = 0x03`, JSON `{ rows, cols }` payload). */\n sendResize(slot: number, rows: number, cols: number): void {\n this.daemon.sendBinary(encodeTerminalFrame({ opcode: \"Resize\", slot, rows, cols }));\n }\n\n // ─── Inbound dispatch ───────────────────────────────────────────────────────\n\n private dispatch(frame: TerminalFrame): void {\n const subscriber = this.subscribers.get(frame.slot);\n if (!subscriber) return; // no subscriber for this slot — drop\n switch (frame.opcode) {\n case \"Output\":\n subscriber.onOutput?.(frame.data);\n return;\n case \"Snapshot\":\n subscriber.onSnapshot?.(frame.data);\n return;\n case \"Restore\":\n subscriber.onRestore?.(frame.data);\n return;\n default:\n // Input/Resize are outbound-only; ignore if echoed back.\n return;\n }\n }\n}\n","/**\n * TerminalPanel — @xterm/xterm mount + binary-frame streaming via `TerminalStreamRouter`\n * (POC `initTerminalPanel`, POC_TO_APP_PLAN_UI.md §4.6). Strict upgrade over the POC's 800ms\n * `capture_terminal_request` poll: the daemon pushes `Output`/`Snapshot`/`Restore` binary frames\n * directly over the one shared `DaemonClient` connection, demuxed by slot.\n *\n * Slot lifecycle: created once via `create_terminal_request`, then persisted onto the tab's\n * `TerminalTabData.slot` via `useTabStore.getState().updateData` so switching away and back to\n * this tab (kept mounted-but-hidden by `TabPanelHost`) never recreates the terminal. `TabPanelHost`\n * only unmounts a tab's panel when the tab leaves the store's `tabs[]` (i.e. real tab close, never\n * a tab switch) — this component's true-unmount effect below relies on exactly that invariant to\n * send `kill_terminal_request`, terminating the PTY server-side instead of leaking it forever.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Terminal } from \"@xterm/xterm\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport \"@xterm/xterm/css/xterm.css\";\nimport type { DaemonClient } from \"@av-pi-studio/client\";\nimport { TerminalStreamRouter } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { Tab, TerminalTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport styles from \"./TerminalPanel.module.css\";\n\nexport interface TerminalPanelProps {\n tab: Tab;\n}\n\ninterface CreateTerminalResponse {\n terminal: { slot: number };\n}\n\n// One TerminalStreamRouter per daemon connection — multiple terminal tabs share it rather than\n// each opening its own frame demuxer over the same socket.\nconst routerByDaemon = new WeakMap<DaemonClient, TerminalStreamRouter>();\n\nfunction routerFor(daemon: DaemonClient): TerminalStreamRouter {\n let router = routerByDaemon.get(daemon);\n if (!router) {\n router = new TerminalStreamRouter(daemon);\n router.start();\n routerByDaemon.set(daemon, router);\n }\n return router;\n}\n\n/** Dark palette matching the app's github-dark-ish default theme (theme/variants.ts \"dark\"). */\nconst TERMINAL_THEME = {\n background: \"#181b1a\",\n foreground: \"#fafafa\",\n cursor: \"#a2b4d7\",\n cursorAccent: \"#181b1a\",\n selectionBackground: \"rgba(255,255,255,0.18)\",\n black: \"#18181b\",\n red: \"#ef4444\",\n green: \"#22c55e\",\n yellow: \"#f59e0b\",\n blue: \"#3b82f6\",\n magenta: \"#a855f7\",\n cyan: \"#14b8a6\",\n white: \"#d4d4d8\",\n brightBlack: \"#52525b\",\n brightRed: \"#f87171\",\n brightGreen: \"#4ade80\",\n brightYellow: \"#fbbf24\",\n brightBlue: \"#60a5fa\",\n brightMagenta: \"#c084fc\",\n brightCyan: \"#2dd4bf\",\n brightWhite: \"#fafafa\",\n};\n\nexport function TerminalPanel({ tab }: TerminalPanelProps) {\n const data = tab.data as TerminalTabData;\n const client = useConnectionStore((s) => s.client);\n const activeTabId = useTabStore((s) => s.activeTabId);\n const isActive = activeTabId === tab.id;\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const terminalRef = useRef<Terminal | null>(null);\n const fitAddonRef = useRef<FitAddon | null>(null);\n const slotRef = useRef<number | null>(data.slot);\n // Mirrors `use-checkout-status.ts`'s convention: kept in sync every render so the unmount-only\n // kill effect below always sends the CURRENT client, never a stale mount-time closure (e.g.\n // after a reconnect swaps in a new `PiStudioClient` instance).\n const clientRef = useRef(client);\n clientRef.current = client;\n\n const [slot, setSlot] = useState<number | null>(data.slot);\n const [error, setError] = useState<string | null>(null);\n\n // ─── Slot lifecycle: create once, persist onto the tab so re-opening reuses it ─────────────\n // React StrictMode double-invokes effects in dev: mount → cleanup → remount, synchronously,\n // on the SAME component instance (refs/state persist across all three phases — this is not\n // three separate mounts). Two things must hold across that: (1) the request fires exactly\n // once, and (2) whether to APPLY the eventual response is decided by whether the component is\n // mounted at RESPONSE time, not by a flag captured at REQUEST time.\n //\n // `requestStartedRef` gives (1): set the instant the request fires and never reset, so the\n // phantom-mount's cleanup-then-remount sees it's already in flight and never fires a second\n // `create_terminal_request` (this is what previously spawned two real PTYs from one Ctrl+T).\n //\n // `isMountedRef` gives (2): flipped true at the START of every effect invocation and false in\n // every cleanup, so it always reflects the LATEST phase. StrictMode's remount happens\n // synchronously, before the request's promise can possibly settle, so by response time\n // `isMountedRef.current` is back to `true` for a StrictMode phantom (correctly applies the\n // slot) — but stays `false` for a genuine fast real close (correctly kills the orphaned PTY\n // instead of leaking it or, as the previous buggy version did, killing a terminal that was\n // never actually torn down).\n const isMountedRef = useRef(false);\n const requestStartedRef = useRef(false);\n useEffect(() => {\n isMountedRef.current = true;\n if (!client || slotRef.current !== null || requestStartedRef.current) {\n return () => {\n isMountedRef.current = false;\n };\n }\n requestStartedRef.current = true;\n\n const cwd = data.cwd || \"~\";\n\n void client.connection\n .request<CreateTerminalResponse>(\"create_terminal_request\", { workspaceId: \"\", cwd })\n .then((res) => {\n const created = res.terminal.slot;\n if (!isMountedRef.current) {\n // A real close happened with no remount after it — kill the PTY that finished\n // spawning after the tab was already gone, instead of leaking it.\n void client.connection.request(\"kill_terminal_request\", { slot: created }).catch(() => {});\n return;\n }\n slotRef.current = created;\n setSlot(created);\n useTabStore.getState().updateData(tab.id, { slot: created });\n })\n .catch((err: unknown) => {\n if (!isMountedRef.current) return;\n setError(err instanceof Error ? err.message : String(err));\n })\n .finally(() => {\n // Reset only after the promise settles — by then StrictMode's synchronous\n // mount→cleanup→remount window has long passed, so this can never reopen the\n // double-fire race. It DOES allow a legitimate retry (e.g. `client` changed because of\n // a reconnect after the first attempt failed) instead of leaving the tab stuck forever.\n requestStartedRef.current = false;\n });\n\n return () => {\n isMountedRef.current = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, tab.id]);\n\n // ─── True unmount only: kill the PTY server-side ───────────────────────────────────────────\n // Runs its cleanup exactly once, when this component itself unmounts (real tab close, per\n // `TabPanelHost`'s \"hidden but alive\" model — a tab switch never unmounts). An empty deps array\n // means React never re-runs the effect body itself; only the cleanup fires, and only on\n // unmount, so this never races the slot-creation effect above or double-kills on a client\n // change. Without this, every closed terminal tab leaked its PTY process forever.\n //\n // For a REATTACH (this tab opened with a non-null `data.slot` from the very first render —\n // `use-terminal-restore.ts`, or the create-effect above once it has resolved) `slotRef.current`\n // is non-null from the start, so StrictMode's synchronous mount→cleanup→remount phantom cycle\n // would fire this cleanup and kill the PTY immediately on mount — unlike a freshly created\n // terminal, where `slotRef.current` is still `null` during that same phantom window (see the\n // create-effect's own comment) and so never hits this path. Deferred via `setTimeout`, exactly\n // like the create-effect's response handler: by the time it fires, StrictMode's remount has\n // already flipped `isMountedRef` back to `true` if this was a phantom unmount, so the kill is\n // skipped; a genuine close never remounts, so `isMountedRef` stays `false` and the kill proceeds.\n useEffect(() => {\n return () => {\n const currentSlot = slotRef.current;\n if (currentSlot === null) return;\n setTimeout(() => {\n if (isMountedRef.current) return; // StrictMode remounted synchronously — not a real close\n void clientRef.current?.connection\n .request(\"kill_terminal_request\", { slot: currentSlot })\n .catch(() => {});\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ─── xterm mount (once slot + container are ready) ─────────────────────────────────────────\n useEffect(() => {\n if (!client || slot === null || !containerRef.current) return;\n\n const terminal = new Terminal({\n cursorBlink: true,\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n fontSize: 13,\n scrollback: 5000,\n theme: TERMINAL_THEME,\n allowProposedApi: true,\n });\n const fitAddon = new FitAddon();\n terminal.loadAddon(fitAddon);\n terminal.open(containerRef.current);\n fitAddon.fit();\n\n terminalRef.current = terminal;\n fitAddonRef.current = fitAddon;\n\n const router = routerFor(client.connection);\n const unsubscribeSlot = router.subscribeSlot(slot, {\n onOutput: (chunk) => terminal.write(chunk),\n onSnapshot: (chunk) => {\n terminal.clear();\n terminal.write(chunk);\n },\n onRestore: (chunk) => terminal.write(chunk),\n });\n\n void client.connection.request(\"subscribe_terminal_request\", { slot }).catch((err: unknown) => {\n setError(err instanceof Error ? err.message : String(err));\n });\n\n const dataDisposable = terminal.onData((chunk) => {\n router.sendInput(slot, new TextEncoder().encode(chunk));\n });\n const resizeDisposable = terminal.onResize(({ cols, rows }) => {\n router.sendResize(slot, rows, cols);\n });\n\n const resizeObserver = new ResizeObserver(() => {\n fitAddon.fit();\n });\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n dataDisposable.dispose();\n resizeDisposable.dispose();\n unsubscribeSlot();\n void client.connection.request(\"unsubscribe_terminal_request\", { slot }).catch(() => {});\n terminal.dispose();\n terminalRef.current = null;\n fitAddonRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, slot]);\n\n // ─── Re-fit whenever this tab becomes the active (visible) one ────────────────────────────\n useEffect(() => {\n if (!isActive) return;\n fitAddonRef.current?.fit();\n }, [isActive]);\n\n if (error) {\n return (\n <div className={styles.wrap}>\n <div className={styles.status}>Terminal error: {error}</div>\n </div>\n );\n }\n\n if (slot === null) {\n return (\n <div className={styles.wrap}>\n <div className={styles.status}>\n <Spinner size=\"sm\" /> Starting terminal…\n </div>\n </div>\n );\n }\n\n return (\n <div className={styles.wrap}>\n <div ref={containerRef} className={styles.terminal} />\n </div>\n );\n}\n"],"names":["TerminalStreamRouter","daemon","__publicField","frame","_a","slot","subscriber","data","encodeTerminalFrame","rows","cols","_b","_c","routerByDaemon","routerFor","router","TERMINAL_THEME","TerminalPanel","tab","client","useConnectionStore","s","isActive","useTabStore","containerRef","useRef","terminalRef","fitAddonRef","slotRef","clientRef","setSlot","useState","error","setError","isMountedRef","requestStartedRef","useEffect","cwd","res","created","err","currentSlot","terminal","Terminal","fitAddon","FitAddon","unsubscribeSlot","chunk","dataDisposable","resizeDisposable","resizeObserver","jsx","styles","jsxs","Spinner"],"mappings":"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"}
1
+ {"version":3,"file":"TerminalPanel-CXsU1Xtg.js","sources":["../../../../client/src/terminal-stream-router.ts","../../../src/features/terminal/TerminalPanel.tsx"],"sourcesContent":["import { encodeTerminalFrame, type TerminalFrame } from \"@av-pi-studio/protocol\";\n\nimport type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Client-side demux of binary terminal frames to per-slot subscribers, plus outbound input/resize\n * encoding (architecture/client-app-runtime.md § Router; features/terminals.md § Binary stream\n * protocol).\n *\n * Inbound `Output`/`Snapshot`/`Restore` frames are dispatched to the subscriber registered for that\n * `slot`. Outbound `Input`/`Resize` are encoded with the right opcode + slot and sent on the data\n * path.\n */\n\nexport interface TerminalSlotSubscriber {\n /** Live terminal output bytes. */\n onOutput?: (data: Uint8Array) => void;\n /** Full-screen snapshot bytes (sent on (re)subscribe). */\n onSnapshot?: (data: Uint8Array) => void;\n /** Restore snapshot bytes (reflowable/mode-gated). */\n onRestore?: (data: Uint8Array) => void;\n}\n\nexport class TerminalStreamRouter {\n private readonly subscribers = new Map<number, TerminalSlotSubscriber>();\n private detach: (() => void) | null = null;\n\n constructor(private readonly daemon: DaemonClient) {}\n\n /** Begin routing inbound terminal frames. Idempotent. */\n start(): void {\n if (this.detach) return;\n this.detach = this.daemon.onTerminalFrame((frame) => this.dispatch(frame));\n }\n\n /** Stop routing inbound frames (subscribers retained). */\n stop(): void {\n this.detach?.();\n this.detach = null;\n }\n\n /** Register (or replace) the subscriber for a slot. Returns an unsubscribe fn. */\n subscribeSlot(slot: number, subscriber: TerminalSlotSubscriber): () => void {\n this.subscribers.set(slot, subscriber);\n return () => {\n if (this.subscribers.get(slot) === subscriber) this.subscribers.delete(slot);\n };\n }\n\n /** True iff a subscriber is registered for the slot. */\n hasSlot(slot: number): boolean {\n return this.subscribers.has(slot);\n }\n\n // ─── Outbound ─────────────────────────────────────────────────────────────\n\n /** Send raw input bytes to a slot's PTY (opcode `Input = 0x02`). */\n sendInput(slot: number, data: Uint8Array): void {\n this.daemon.sendBinary(encodeTerminalFrame({ opcode: \"Input\", slot, data }));\n }\n\n /** Send a resize (opcode `Resize = 0x03`, JSON `{ rows, cols }` payload). */\n sendResize(slot: number, rows: number, cols: number): void {\n this.daemon.sendBinary(encodeTerminalFrame({ opcode: \"Resize\", slot, rows, cols }));\n }\n\n // ─── Inbound dispatch ───────────────────────────────────────────────────────\n\n private dispatch(frame: TerminalFrame): void {\n const subscriber = this.subscribers.get(frame.slot);\n if (!subscriber) return; // no subscriber for this slot — drop\n switch (frame.opcode) {\n case \"Output\":\n subscriber.onOutput?.(frame.data);\n return;\n case \"Snapshot\":\n subscriber.onSnapshot?.(frame.data);\n return;\n case \"Restore\":\n subscriber.onRestore?.(frame.data);\n return;\n default:\n // Input/Resize are outbound-only; ignore if echoed back.\n return;\n }\n }\n}\n","/**\n * TerminalPanel — @xterm/xterm mount + binary-frame streaming via `TerminalStreamRouter`\n * (POC `initTerminalPanel`, POC_TO_APP_PLAN_UI.md §4.6). Strict upgrade over the POC's 800ms\n * `capture_terminal_request` poll: the daemon pushes `Output`/`Snapshot`/`Restore` binary frames\n * directly over the one shared `DaemonClient` connection, demuxed by slot.\n *\n * Slot lifecycle: created once via `create_terminal_request`, then persisted onto the tab's\n * `TerminalTabData.slot` via `useTabStore.getState().updateData` so switching away and back to\n * this tab (kept mounted-but-hidden by `TabPanelHost`) never recreates the terminal. `TabPanelHost`\n * only unmounts a tab's panel when the tab leaves the store's `tabs[]` (i.e. real tab close, never\n * a tab switch) — this component's true-unmount effect below relies on exactly that invariant to\n * send `kill_terminal_request`, terminating the PTY server-side instead of leaking it forever.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Terminal } from \"@xterm/xterm\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport \"@xterm/xterm/css/xterm.css\";\nimport type { DaemonClient } from \"@av-pi-studio/client\";\nimport { TerminalStreamRouter } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { Tab, TerminalTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport styles from \"./TerminalPanel.module.css\";\n\nexport interface TerminalPanelProps {\n tab: Tab;\n}\n\ninterface CreateTerminalResponse {\n terminal: { slot: number };\n}\n\n// One TerminalStreamRouter per daemon connection — multiple terminal tabs share it rather than\n// each opening its own frame demuxer over the same socket.\nconst routerByDaemon = new WeakMap<DaemonClient, TerminalStreamRouter>();\n\nfunction routerFor(daemon: DaemonClient): TerminalStreamRouter {\n let router = routerByDaemon.get(daemon);\n if (!router) {\n router = new TerminalStreamRouter(daemon);\n router.start();\n routerByDaemon.set(daemon, router);\n }\n return router;\n}\n\n/** Dark palette matching the app's github-dark-ish default theme (theme/variants.ts \"dark\"). */\nconst TERMINAL_THEME = {\n background: \"#181b1a\",\n foreground: \"#fafafa\",\n cursor: \"#a2b4d7\",\n cursorAccent: \"#181b1a\",\n selectionBackground: \"rgba(255,255,255,0.18)\",\n black: \"#18181b\",\n red: \"#ef4444\",\n green: \"#22c55e\",\n yellow: \"#f59e0b\",\n blue: \"#3b82f6\",\n magenta: \"#a855f7\",\n cyan: \"#14b8a6\",\n white: \"#d4d4d8\",\n brightBlack: \"#52525b\",\n brightRed: \"#f87171\",\n brightGreen: \"#4ade80\",\n brightYellow: \"#fbbf24\",\n brightBlue: \"#60a5fa\",\n brightMagenta: \"#c084fc\",\n brightCyan: \"#2dd4bf\",\n brightWhite: \"#fafafa\",\n};\n\nexport function TerminalPanel({ tab }: TerminalPanelProps) {\n const data = tab.data as TerminalTabData;\n const client = useConnectionStore((s) => s.client);\n const activeTabId = useTabStore((s) => s.activeTabId);\n const isActive = activeTabId === tab.id;\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const terminalRef = useRef<Terminal | null>(null);\n const fitAddonRef = useRef<FitAddon | null>(null);\n const slotRef = useRef<number | null>(data.slot);\n // Mirrors `use-checkout-status.ts`'s convention: kept in sync every render so the unmount-only\n // kill effect below always sends the CURRENT client, never a stale mount-time closure (e.g.\n // after a reconnect swaps in a new `PiStudioClient` instance).\n const clientRef = useRef(client);\n clientRef.current = client;\n\n const [slot, setSlot] = useState<number | null>(data.slot);\n const [error, setError] = useState<string | null>(null);\n\n // ─── Slot lifecycle: create once, persist onto the tab so re-opening reuses it ─────────────\n // React StrictMode double-invokes effects in dev: mount → cleanup → remount, synchronously,\n // on the SAME component instance (refs/state persist across all three phases — this is not\n // three separate mounts). Two things must hold across that: (1) the request fires exactly\n // once, and (2) whether to APPLY the eventual response is decided by whether the component is\n // mounted at RESPONSE time, not by a flag captured at REQUEST time.\n //\n // `requestStartedRef` gives (1): set the instant the request fires and never reset, so the\n // phantom-mount's cleanup-then-remount sees it's already in flight and never fires a second\n // `create_terminal_request` (this is what previously spawned two real PTYs from one Ctrl+T).\n //\n // `isMountedRef` gives (2): flipped true at the START of every effect invocation and false in\n // every cleanup, so it always reflects the LATEST phase. StrictMode's remount happens\n // synchronously, before the request's promise can possibly settle, so by response time\n // `isMountedRef.current` is back to `true` for a StrictMode phantom (correctly applies the\n // slot) — but stays `false` for a genuine fast real close (correctly kills the orphaned PTY\n // instead of leaking it or, as the previous buggy version did, killing a terminal that was\n // never actually torn down).\n const isMountedRef = useRef(false);\n const requestStartedRef = useRef(false);\n useEffect(() => {\n isMountedRef.current = true;\n if (!client || slotRef.current !== null || requestStartedRef.current) {\n return () => {\n isMountedRef.current = false;\n };\n }\n requestStartedRef.current = true;\n\n const cwd = data.cwd || \"~\";\n\n void client.connection\n .request<CreateTerminalResponse>(\"create_terminal_request\", { workspaceId: \"\", cwd })\n .then((res) => {\n const created = res.terminal.slot;\n if (!isMountedRef.current) {\n // A real close happened with no remount after it — kill the PTY that finished\n // spawning after the tab was already gone, instead of leaking it.\n void client.connection.request(\"kill_terminal_request\", { slot: created }).catch(() => {});\n return;\n }\n slotRef.current = created;\n setSlot(created);\n useTabStore.getState().updateData(tab.id, { slot: created });\n })\n .catch((err: unknown) => {\n if (!isMountedRef.current) return;\n setError(err instanceof Error ? err.message : String(err));\n })\n .finally(() => {\n // Reset only after the promise settles — by then StrictMode's synchronous\n // mount→cleanup→remount window has long passed, so this can never reopen the\n // double-fire race. It DOES allow a legitimate retry (e.g. `client` changed because of\n // a reconnect after the first attempt failed) instead of leaving the tab stuck forever.\n requestStartedRef.current = false;\n });\n\n return () => {\n isMountedRef.current = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, tab.id]);\n\n // ─── True unmount only: kill the PTY server-side ───────────────────────────────────────────\n // Runs its cleanup exactly once, when this component itself unmounts (real tab close, per\n // `TabPanelHost`'s \"hidden but alive\" model — a tab switch never unmounts). An empty deps array\n // means React never re-runs the effect body itself; only the cleanup fires, and only on\n // unmount, so this never races the slot-creation effect above or double-kills on a client\n // change. Without this, every closed terminal tab leaked its PTY process forever.\n //\n // For a REATTACH (this tab opened with a non-null `data.slot` from the very first render —\n // `use-terminal-restore.ts`, or the create-effect above once it has resolved) `slotRef.current`\n // is non-null from the start, so StrictMode's synchronous mount→cleanup→remount phantom cycle\n // would fire this cleanup and kill the PTY immediately on mount — unlike a freshly created\n // terminal, where `slotRef.current` is still `null` during that same phantom window (see the\n // create-effect's own comment) and so never hits this path. Deferred via `setTimeout`, exactly\n // like the create-effect's response handler: by the time it fires, StrictMode's remount has\n // already flipped `isMountedRef` back to `true` if this was a phantom unmount, so the kill is\n // skipped; a genuine close never remounts, so `isMountedRef` stays `false` and the kill proceeds.\n useEffect(() => {\n return () => {\n const currentSlot = slotRef.current;\n if (currentSlot === null) return;\n setTimeout(() => {\n if (isMountedRef.current) return; // StrictMode remounted synchronously — not a real close\n void clientRef.current?.connection\n .request(\"kill_terminal_request\", { slot: currentSlot })\n .catch(() => {});\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ─── xterm mount (once slot + container are ready) ─────────────────────────────────────────\n useEffect(() => {\n if (!client || slot === null || !containerRef.current) return;\n\n const terminal = new Terminal({\n cursorBlink: true,\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n fontSize: 13,\n scrollback: 5000,\n theme: TERMINAL_THEME,\n allowProposedApi: true,\n });\n const fitAddon = new FitAddon();\n terminal.loadAddon(fitAddon);\n terminal.open(containerRef.current);\n fitAddon.fit();\n\n terminalRef.current = terminal;\n fitAddonRef.current = fitAddon;\n\n const router = routerFor(client.connection);\n const unsubscribeSlot = router.subscribeSlot(slot, {\n onOutput: (chunk) => terminal.write(chunk),\n onSnapshot: (chunk) => {\n terminal.clear();\n terminal.write(chunk);\n },\n onRestore: (chunk) => terminal.write(chunk),\n });\n\n void client.connection.request(\"subscribe_terminal_request\", { slot }).catch((err: unknown) => {\n setError(err instanceof Error ? err.message : String(err));\n });\n\n const dataDisposable = terminal.onData((chunk) => {\n router.sendInput(slot, new TextEncoder().encode(chunk));\n });\n const resizeDisposable = terminal.onResize(({ cols, rows }) => {\n router.sendResize(slot, rows, cols);\n });\n\n const resizeObserver = new ResizeObserver(() => {\n fitAddon.fit();\n });\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n dataDisposable.dispose();\n resizeDisposable.dispose();\n unsubscribeSlot();\n void client.connection.request(\"unsubscribe_terminal_request\", { slot }).catch(() => {});\n terminal.dispose();\n terminalRef.current = null;\n fitAddonRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, slot]);\n\n // ─── Re-fit whenever this tab becomes the active (visible) one ────────────────────────────\n useEffect(() => {\n if (!isActive) return;\n fitAddonRef.current?.fit();\n }, [isActive]);\n\n if (error) {\n return (\n <div className={styles.wrap}>\n <div className={styles.status}>Terminal error: {error}</div>\n </div>\n );\n }\n\n if (slot === null) {\n return (\n <div className={styles.wrap}>\n <div className={styles.status}>\n <Spinner size=\"sm\" /> Starting terminal…\n </div>\n </div>\n );\n }\n\n return (\n <div className={styles.wrap}>\n <div ref={containerRef} className={styles.terminal} />\n </div>\n );\n}\n"],"names":["TerminalStreamRouter","daemon","__publicField","frame","_a","slot","subscriber","data","encodeTerminalFrame","rows","cols","_b","_c","routerByDaemon","routerFor","router","TERMINAL_THEME","TerminalPanel","tab","client","useConnectionStore","s","isActive","useTabStore","containerRef","useRef","terminalRef","fitAddonRef","slotRef","clientRef","setSlot","useState","error","setError","isMountedRef","requestStartedRef","useEffect","cwd","res","created","err","currentSlot","terminal","Terminal","fitAddon","FitAddon","unsubscribeSlot","chunk","dataDisposable","resizeDisposable","resizeObserver","jsx","styles","jsxs","Spinner"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{r as N,j as r}from"./vendor-react-p-LLqNcx.js";import{r as _,S as u,B as j}from"./index-DneBC_ta.js";import{u as b,F as k,C as y}from"./CodeView-DwUKpXwU.js";import{u as z}from"./vendor-query-chxBrUuj.js";import{u as E}from"./use-file-download-DsIqcxhB.js";import{p as d}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";function h(s){const{enabled:n,download:e,decode:o}=s;return{isLoading:n&&(e.isLoading||e.hasObjectUrl&&o.isLoading),isError:e.isError||o.isError,error:e.isError?e.error:o.error,data:o.data}}function v(s,n=!0){var a;const e=E(s,n),o=(a=e.data)==null?void 0:a.objectUrl,t=z({queryKey:_.fileText(s,o??null),queryFn:async()=>({content:await(await fetch(o)).text()}),enabled:n&&!!o});return h({enabled:n,download:{isLoading:e.isLoading,isError:e.isError,error:e.error,hasObjectUrl:!!o},decode:{isLoading:t.isLoading,isError:t.isError,error:t.error,data:t.data}})}function U(s){const{maxDisplayBytes:n,inline:e,streamed:o,download:t}=s;if(e.isLoading)return{kind:"loading"};if(e.tooLarge){const{size:a}=e.tooLarge;return a>n?{kind:"too-large",size:a,maxDisplayBytes:n,downloading:t.requested&&t.isLoading,downloadUrl:t.objectUrl,downloadName:t.fileName}:o.isLoading?{kind:"streaming",size:a}:o.isError?{kind:"stream-error",size:a,message:o.errorMessage??"stream failed"}:o.content!==null?{kind:"streamed",size:a,content:o.content}:{kind:"loading"}}return e.isError?{kind:"error",message:e.errorMessage??"unknown error"}:e.content!==null?{kind:"inline",content:e.content}:{kind:"loading"}}const B="_wrap_150m3_1",F="_note_150m3_7",D="_body_150m3_15",M="_tooLarge_150m3_19",l={wrap:B,note:F,body:D,tooLarge:M},L=30*1024*1024;function c(s){return`${(s/(1024*1024)).toFixed(1)} MB`}function X({path:s}){var f,x,p,w;const n=b(s),e=n.error instanceof k?n.error:null,o=e!==null&&e.size<=L,t=v(s,o),[a,g]=N.useState(!1),m=E(s,a),i=U({maxDisplayBytes:L,inline:{isLoading:n.isLoading,isError:n.isError,tooLarge:e?{size:e.size}:null,errorMessage:n.error instanceof Error?n.error.message:null,content:((f=n.data)==null?void 0:f.content)??null},streamed:{isLoading:t.isLoading,isError:t.isError,errorMessage:t.error instanceof Error?t.error.message:null,content:((x=t.data)==null?void 0:x.content)??null},download:{requested:a,isLoading:m.isLoading,objectUrl:((p=m.data)==null?void 0:p.objectUrl)??null,fileName:((w=m.data)==null?void 0:w.fileName)??null}});switch(i.kind){case"loading":return r.jsxs("div",{className:d.emptyState,children:[r.jsx(u,{size:"sm"})," Loading..."]});case"inline":return r.jsx(y,{path:s,content:i.content});case"streaming":return r.jsxs("div",{className:d.emptyState,children:[r.jsx(u,{size:"sm"})," Streaming ",c(i.size)," file..."]});case"stream-error":return r.jsxs("div",{className:d.emptyState,children:["Error: ",i.message]});case"streamed":return r.jsxs("div",{className:l.wrap,children:[r.jsxs("div",{className:l.note,children:[c(i.size)," file streamed"]}),r.jsx("div",{className:l.body,children:r.jsx(y,{path:s,content:i.content})})]});case"too-large":{const S=s.split("/").pop()||s;return r.jsx("div",{className:d.emptyState,children:r.jsxs("div",{className:l.tooLarge,children:[r.jsxs("div",{children:[c(i.size)," — too large to display (display ceiling is"," ",c(i.maxDisplayBytes),")."]}),i.downloading?r.jsx(u,{size:"sm"}):i.downloadUrl?r.jsx("a",{href:i.downloadUrl,download:i.downloadName||S,children:r.jsx(j,{size:"sm",children:"Save file"})}):r.jsx(j,{size:"sm",onClick:()=>g(!0),children:"Download"})]})})}case"error":return r.jsxs("div",{className:d.emptyState,children:["Error: ",i.message]})}}export{L as MAX_DISPLAY_BYTES,X as TextViewer};
2
- //# sourceMappingURL=TextViewer-CDXNfUrU.js.map
1
+ import{r as N,j as r}from"./vendor-react-p-LLqNcx.js";import{r as _,S as u,B as j}from"./index-DulchPgH.js";import{u as b,F as k,C as y}from"./CodeView-DRJLXH-w.js";import{u as z}from"./vendor-query-chxBrUuj.js";import{u as E}from"./use-file-download-AaY3793H.js";import{p as d}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";function h(s){const{enabled:n,download:e,decode:o}=s;return{isLoading:n&&(e.isLoading||e.hasObjectUrl&&o.isLoading),isError:e.isError||o.isError,error:e.isError?e.error:o.error,data:o.data}}function v(s,n=!0){var a;const e=E(s,n),o=(a=e.data)==null?void 0:a.objectUrl,t=z({queryKey:_.fileText(s,o??null),queryFn:async()=>({content:await(await fetch(o)).text()}),enabled:n&&!!o});return h({enabled:n,download:{isLoading:e.isLoading,isError:e.isError,error:e.error,hasObjectUrl:!!o},decode:{isLoading:t.isLoading,isError:t.isError,error:t.error,data:t.data}})}function U(s){const{maxDisplayBytes:n,inline:e,streamed:o,download:t}=s;if(e.isLoading)return{kind:"loading"};if(e.tooLarge){const{size:a}=e.tooLarge;return a>n?{kind:"too-large",size:a,maxDisplayBytes:n,downloading:t.requested&&t.isLoading,downloadUrl:t.objectUrl,downloadName:t.fileName}:o.isLoading?{kind:"streaming",size:a}:o.isError?{kind:"stream-error",size:a,message:o.errorMessage??"stream failed"}:o.content!==null?{kind:"streamed",size:a,content:o.content}:{kind:"loading"}}return e.isError?{kind:"error",message:e.errorMessage??"unknown error"}:e.content!==null?{kind:"inline",content:e.content}:{kind:"loading"}}const B="_wrap_150m3_1",F="_note_150m3_7",D="_body_150m3_15",M="_tooLarge_150m3_19",l={wrap:B,note:F,body:D,tooLarge:M},L=30*1024*1024;function c(s){return`${(s/(1024*1024)).toFixed(1)} MB`}function X({path:s}){var f,x,p,w;const n=b(s),e=n.error instanceof k?n.error:null,o=e!==null&&e.size<=L,t=v(s,o),[a,g]=N.useState(!1),m=E(s,a),i=U({maxDisplayBytes:L,inline:{isLoading:n.isLoading,isError:n.isError,tooLarge:e?{size:e.size}:null,errorMessage:n.error instanceof Error?n.error.message:null,content:((f=n.data)==null?void 0:f.content)??null},streamed:{isLoading:t.isLoading,isError:t.isError,errorMessage:t.error instanceof Error?t.error.message:null,content:((x=t.data)==null?void 0:x.content)??null},download:{requested:a,isLoading:m.isLoading,objectUrl:((p=m.data)==null?void 0:p.objectUrl)??null,fileName:((w=m.data)==null?void 0:w.fileName)??null}});switch(i.kind){case"loading":return r.jsxs("div",{className:d.emptyState,children:[r.jsx(u,{size:"sm"})," Loading..."]});case"inline":return r.jsx(y,{path:s,content:i.content});case"streaming":return r.jsxs("div",{className:d.emptyState,children:[r.jsx(u,{size:"sm"})," Streaming ",c(i.size)," file..."]});case"stream-error":return r.jsxs("div",{className:d.emptyState,children:["Error: ",i.message]});case"streamed":return r.jsxs("div",{className:l.wrap,children:[r.jsxs("div",{className:l.note,children:[c(i.size)," file streamed"]}),r.jsx("div",{className:l.body,children:r.jsx(y,{path:s,content:i.content})})]});case"too-large":{const S=s.split("/").pop()||s;return r.jsx("div",{className:d.emptyState,children:r.jsxs("div",{className:l.tooLarge,children:[r.jsxs("div",{children:[c(i.size)," — too large to display (display ceiling is"," ",c(i.maxDisplayBytes),")."]}),i.downloading?r.jsx(u,{size:"sm"}):i.downloadUrl?r.jsx("a",{href:i.downloadUrl,download:i.downloadName||S,children:r.jsx(j,{size:"sm",children:"Save file"})}):r.jsx(j,{size:"sm",onClick:()=>g(!0),children:"Download"})]})})}case"error":return r.jsxs("div",{className:d.emptyState,children:["Error: ",i.message]})}}export{L as MAX_DISPLAY_BYTES,X as TextViewer};
2
+ //# sourceMappingURL=TextViewer-DHDR_OoU.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextViewer-CDXNfUrU.js","sources":["../../../src/hooks/file-text-state.ts","../../../src/hooks/use-file-text.ts","../../../src/features/files/text-viewer-state.ts","../../../src/features/files/TextViewer.tsx"],"sourcesContent":["/**\n * Pure query-state merge for `use-file-text` (task-009), kept DOM/React-free so the\n * loading/error/data composition across the two dependent queries it wraps (download → decode)\n * is unit-testable directly, without `renderHook` (no jsdom test environment in this repo).\n */\n\nexport interface FileTextResult {\n content: string;\n}\n\nexport interface FileTextMergeInputs {\n enabled: boolean;\n download: { isLoading: boolean; isError: boolean; error: unknown; hasObjectUrl: boolean };\n decode: {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n };\n}\n\nexport interface FileTextQueryState {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n}\n\nexport function mergeFileTextState(input: FileTextMergeInputs): FileTextQueryState {\n const { enabled, download, decode } = input;\n return {\n isLoading: enabled && (download.isLoading || (download.hasObjectUrl && decode.isLoading)),\n isError: download.isError || decode.isError,\n error: download.isError ? download.error : decode.error,\n data: decode.data,\n };\n}\n","/**\n * `use-file-text` — tier-2 fallback for `TextViewer` (task-009): decodes a file's bytes to UTF-8\n * text via the already-uncapped chunked binary download path (`useFileDownload` →\n * `file_download_token_request`/`file_download_request`), for files over `file_read_request`'s\n * inline cap (`MAX_INLINE_FILE_READ_BYTES`, `packages/server/src/files/limits.ts`). Same\n * transport the molecule viewer uses for its own (binary-safe) source — no new RPC, no new\n * transport, just a text decode over the resulting blob.\n *\n * Note the daemon asymmetry documented on `useFileDownload`'s own RPCs: the download path is\n * registered only in the production bootstrap (`bootstrap.ts`), not `dev-bootstrap.ts`. Under\n * `npm run dev:daemon` a file above the inline cap fails this fetch rather than rendering — that\n * surfaces as the normal error state below, which is the intended behavior (see task-009 notes).\n *\n * The loading/error/data merge across the two dependent queries (download → decode) is the pure,\n * DOM-free `mergeFileTextState` (`file-text-state.ts`) — this hook is a thin wrapper feeding it\n * live query state.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { useFileDownload } from \"./use-file-download.js\";\nimport { mergeFileTextState, type FileTextResult } from \"./file-text-state.js\";\n\nexport type { FileTextResult };\n\nexport function useFileText(path: string, enabled = true) {\n const download = useFileDownload(path, enabled);\n const objectUrl = download.data?.objectUrl;\n\n const decode = useQuery({\n queryKey: rpcKeys.fileText(path, objectUrl ?? null),\n queryFn: async (): Promise<FileTextResult> => {\n const content = await (await fetch(objectUrl as string)).text();\n return { content };\n },\n enabled: enabled && Boolean(objectUrl),\n });\n\n return mergeFileTextState({\n enabled,\n download: {\n isLoading: download.isLoading,\n isError: download.isError,\n error: download.error,\n hasObjectUrl: Boolean(objectUrl),\n },\n decode: {\n isLoading: decode.isLoading,\n isError: decode.isError,\n error: decode.error,\n data: decode.data,\n },\n });\n}\n","/**\n * Pure tier-selection logic for `TextViewer` (task-009), kept in its own DOM-free module so the\n * three-state branch (inline / streamed / download-only) is unit-testable without mounting\n * CodeMirror or React — mirrors `molecule-reload.ts`'s `shouldApplyRefresh` extraction for the\n * same reason: this repo has no jsdom test environment configured anywhere.\n */\n\nexport type TextViewerState =\n | { kind: \"loading\" }\n | { kind: \"inline\"; content: string }\n | { kind: \"streaming\"; size: number }\n | { kind: \"streamed\"; size: number; content: string }\n | { kind: \"stream-error\"; size: number; message: string }\n | {\n kind: \"too-large\";\n size: number;\n maxDisplayBytes: number;\n downloading: boolean;\n downloadUrl: string | null;\n downloadName: string | null;\n }\n | { kind: \"error\"; message: string };\n\nexport interface TextViewerInputs {\n maxDisplayBytes: number;\n inline: {\n isLoading: boolean;\n isError: boolean;\n /** `{ size }` when the failure was specifically `FileTooLargeError`; `null` for a generic\n * read failure (network/permission/etc). */\n tooLarge: { size: number } | null;\n errorMessage: string | null;\n content: string | null;\n };\n streamed: {\n isLoading: boolean;\n isError: boolean;\n errorMessage: string | null;\n content: string | null;\n };\n download: {\n requested: boolean;\n isLoading: boolean;\n objectUrl: string | null;\n fileName: string | null;\n };\n}\n\n/** Selects exactly one `TextViewerState` from the current query states. Pure — no hooks, no I/O. */\nexport function selectTextViewerState(input: TextViewerInputs): TextViewerState {\n const { maxDisplayBytes, inline, streamed, download } = input;\n\n if (inline.isLoading) return { kind: \"loading\" };\n\n if (inline.tooLarge) {\n const { size } = inline.tooLarge;\n if (size > maxDisplayBytes) {\n return {\n kind: \"too-large\",\n size,\n maxDisplayBytes,\n downloading: download.requested && download.isLoading,\n downloadUrl: download.objectUrl,\n downloadName: download.fileName,\n };\n }\n if (streamed.isLoading) return { kind: \"streaming\", size };\n if (streamed.isError) {\n return { kind: \"stream-error\", size, message: streamed.errorMessage ?? \"stream failed\" };\n }\n if (streamed.content !== null) return { kind: \"streamed\", size, content: streamed.content };\n return { kind: \"loading\" };\n }\n\n if (inline.isError) {\n return { kind: \"error\", message: inline.errorMessage ?? \"unknown error\" };\n }\n\n if (inline.content !== null) return { kind: \"inline\", content: inline.content };\n return { kind: \"loading\" };\n}\n","/**\n * TextViewer — the default viewer for source/text files: fetches the UTF-8 preview via\n * `useFileRead` and renders it through `CodeView` (line gutter + Shiki highlighting). Registered\n * in `viewer-registry.ts` as the fallback for any file not claimed by a more specific viewer.\n *\n * Three size tiers (task-009 — raising `file_read_request`'s old 512 KiB ceiling):\n * 1. `size <= MAX_INLINE_FILE_READ_BYTES` (server-side, 5 MiB) — the `useFileRead` JSON round\n * trip above, unchanged.\n * 2. `MAX_INLINE_FILE_READ_BYTES < size <= MAX_DISPLAY_BYTES` (30 MiB) — transparently refetch via\n * the uncapped chunked binary download path (`use-file-text.ts`) and render the same\n * `CodeView`, with a muted note that the file was streamed rather than read inline.\n * 3. `size > MAX_DISPLAY_BYTES` — a terminal state: no render attempt, just the size, why, and a\n * download action (`BinaryFallbackViewer`'s existing on-demand download pattern).\n *\n * Which tier applies is the pure, DOM-free `selectTextViewerState` (`text-viewer-state.ts`) — this\n * component is a thin switch-render over it.\n */\n\nimport { useState } from \"react\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { useFileRead, FileTooLargeError } from \"@pi-studio-ui/hooks/use-file-read.js\";\nimport { useFileText } from \"@pi-studio-ui/hooks/use-file-text.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { CodeView } from \"./CodeView.js\";\nimport { selectTextViewerState } from \"./text-viewer-state.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport styles from \"./FilePanel.module.css\";\nimport textStyles from \"./TextViewer.module.css\";\n\n/** Display ceiling above the inline cap. `CodeView`'s CodeMirror instance enables\n * `EditorView.lineWrapping`, which makes it measure line heights across the whole document —\n * well past interactive above this size, so files this large get a terminal download-only state\n * instead of an attempted render. Deliberately not configurable (task-009 notes). */\nexport const MAX_DISPLAY_BYTES = 30 * 1024 * 1024;\n\nfunction formatMegabytes(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function TextViewer({ path }: ViewerProps) {\n const inline = useFileRead(path);\n const tooLarge = inline.error instanceof FileTooLargeError ? inline.error : null;\n const displayable = tooLarge !== null && tooLarge.size <= MAX_DISPLAY_BYTES;\n\n const streamed = useFileText(path, displayable);\n const [downloadRequested, setDownloadRequested] = useState(false);\n const download = useFileDownload(path, downloadRequested);\n\n const state = selectTextViewerState({\n maxDisplayBytes: MAX_DISPLAY_BYTES,\n inline: {\n isLoading: inline.isLoading,\n isError: inline.isError,\n tooLarge: tooLarge ? { size: tooLarge.size } : null,\n errorMessage: inline.error instanceof Error ? inline.error.message : null,\n content: inline.data?.content ?? null,\n },\n streamed: {\n isLoading: streamed.isLoading,\n isError: streamed.isError,\n errorMessage: streamed.error instanceof Error ? streamed.error.message : null,\n content: streamed.data?.content ?? null,\n },\n download: {\n requested: downloadRequested,\n isLoading: download.isLoading,\n objectUrl: download.data?.objectUrl ?? null,\n fileName: download.data?.fileName ?? null,\n },\n });\n\n switch (state.kind) {\n case \"loading\":\n return (\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n );\n case \"inline\":\n return <CodeView path={path} content={state.content} />;\n case \"streaming\":\n return (\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Streaming {formatMegabytes(state.size)} file...\n </div>\n );\n case \"stream-error\":\n return <div className={styles.emptyState}>Error: {state.message}</div>;\n case \"streamed\":\n return (\n <div className={textStyles.wrap}>\n <div className={textStyles.note}>{formatMegabytes(state.size)} file streamed</div>\n <div className={textStyles.body}>\n <CodeView path={path} content={state.content} />\n </div>\n </div>\n );\n case \"too-large\": {\n const name = path.split(\"/\").pop() || path;\n return (\n <div className={styles.emptyState}>\n <div className={textStyles.tooLarge}>\n <div>\n {formatMegabytes(state.size)} — too large to display (display ceiling is{\" \"}\n {formatMegabytes(state.maxDisplayBytes)}).\n </div>\n {state.downloading ? (\n <Spinner size=\"sm\" />\n ) : state.downloadUrl ? (\n <a href={state.downloadUrl} download={state.downloadName || name}>\n <Button size=\"sm\">Save file</Button>\n </a>\n ) : (\n <Button size=\"sm\" onClick={() => setDownloadRequested(true)}>\n Download\n </Button>\n )}\n </div>\n </div>\n );\n }\n case \"error\":\n return <div className={styles.emptyState}>Error: {state.message}</div>;\n }\n}\n"],"names":["mergeFileTextState","input","enabled","download","decode","useFileText","path","useFileDownload","objectUrl","_a","useQuery","rpcKeys","selectTextViewerState","maxDisplayBytes","inline","streamed","size","MAX_DISPLAY_BYTES","formatMegabytes","bytes","TextViewer","useFileRead","tooLarge","FileTooLargeError","displayable","downloadRequested","setDownloadRequested","useState","state","_b","_c","_d","jsxs","styles","jsx","Spinner","CodeView","textStyles","name","Button"],"mappings":"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"}
1
+ {"version":3,"file":"TextViewer-DHDR_OoU.js","sources":["../../../src/hooks/file-text-state.ts","../../../src/hooks/use-file-text.ts","../../../src/features/files/text-viewer-state.ts","../../../src/features/files/TextViewer.tsx"],"sourcesContent":["/**\n * Pure query-state merge for `use-file-text` (task-009), kept DOM/React-free so the\n * loading/error/data composition across the two dependent queries it wraps (download → decode)\n * is unit-testable directly, without `renderHook` (no jsdom test environment in this repo).\n */\n\nexport interface FileTextResult {\n content: string;\n}\n\nexport interface FileTextMergeInputs {\n enabled: boolean;\n download: { isLoading: boolean; isError: boolean; error: unknown; hasObjectUrl: boolean };\n decode: {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n };\n}\n\nexport interface FileTextQueryState {\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n data: FileTextResult | undefined;\n}\n\nexport function mergeFileTextState(input: FileTextMergeInputs): FileTextQueryState {\n const { enabled, download, decode } = input;\n return {\n isLoading: enabled && (download.isLoading || (download.hasObjectUrl && decode.isLoading)),\n isError: download.isError || decode.isError,\n error: download.isError ? download.error : decode.error,\n data: decode.data,\n };\n}\n","/**\n * `use-file-text` — tier-2 fallback for `TextViewer` (task-009): decodes a file's bytes to UTF-8\n * text via the already-uncapped chunked binary download path (`useFileDownload` →\n * `file_download_token_request`/`file_download_request`), for files over `file_read_request`'s\n * inline cap (`MAX_INLINE_FILE_READ_BYTES`, `packages/server/src/files/limits.ts`). Same\n * transport the molecule viewer uses for its own (binary-safe) source — no new RPC, no new\n * transport, just a text decode over the resulting blob.\n *\n * Note the daemon asymmetry documented on `useFileDownload`'s own RPCs: the download path is\n * registered only in the production bootstrap (`bootstrap.ts`), not `dev-bootstrap.ts`. Under\n * `npm run dev:daemon` a file above the inline cap fails this fetch rather than rendering — that\n * surfaces as the normal error state below, which is the intended behavior (see task-009 notes).\n *\n * The loading/error/data merge across the two dependent queries (download → decode) is the pure,\n * DOM-free `mergeFileTextState` (`file-text-state.ts`) — this hook is a thin wrapper feeding it\n * live query state.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { useFileDownload } from \"./use-file-download.js\";\nimport { mergeFileTextState, type FileTextResult } from \"./file-text-state.js\";\n\nexport type { FileTextResult };\n\nexport function useFileText(path: string, enabled = true) {\n const download = useFileDownload(path, enabled);\n const objectUrl = download.data?.objectUrl;\n\n const decode = useQuery({\n queryKey: rpcKeys.fileText(path, objectUrl ?? null),\n queryFn: async (): Promise<FileTextResult> => {\n const content = await (await fetch(objectUrl as string)).text();\n return { content };\n },\n enabled: enabled && Boolean(objectUrl),\n });\n\n return mergeFileTextState({\n enabled,\n download: {\n isLoading: download.isLoading,\n isError: download.isError,\n error: download.error,\n hasObjectUrl: Boolean(objectUrl),\n },\n decode: {\n isLoading: decode.isLoading,\n isError: decode.isError,\n error: decode.error,\n data: decode.data,\n },\n });\n}\n","/**\n * Pure tier-selection logic for `TextViewer` (task-009), kept in its own DOM-free module so the\n * three-state branch (inline / streamed / download-only) is unit-testable without mounting\n * CodeMirror or React — mirrors `molecule-reload.ts`'s `shouldApplyRefresh` extraction for the\n * same reason: this repo has no jsdom test environment configured anywhere.\n */\n\nexport type TextViewerState =\n | { kind: \"loading\" }\n | { kind: \"inline\"; content: string }\n | { kind: \"streaming\"; size: number }\n | { kind: \"streamed\"; size: number; content: string }\n | { kind: \"stream-error\"; size: number; message: string }\n | {\n kind: \"too-large\";\n size: number;\n maxDisplayBytes: number;\n downloading: boolean;\n downloadUrl: string | null;\n downloadName: string | null;\n }\n | { kind: \"error\"; message: string };\n\nexport interface TextViewerInputs {\n maxDisplayBytes: number;\n inline: {\n isLoading: boolean;\n isError: boolean;\n /** `{ size }` when the failure was specifically `FileTooLargeError`; `null` for a generic\n * read failure (network/permission/etc). */\n tooLarge: { size: number } | null;\n errorMessage: string | null;\n content: string | null;\n };\n streamed: {\n isLoading: boolean;\n isError: boolean;\n errorMessage: string | null;\n content: string | null;\n };\n download: {\n requested: boolean;\n isLoading: boolean;\n objectUrl: string | null;\n fileName: string | null;\n };\n}\n\n/** Selects exactly one `TextViewerState` from the current query states. Pure — no hooks, no I/O. */\nexport function selectTextViewerState(input: TextViewerInputs): TextViewerState {\n const { maxDisplayBytes, inline, streamed, download } = input;\n\n if (inline.isLoading) return { kind: \"loading\" };\n\n if (inline.tooLarge) {\n const { size } = inline.tooLarge;\n if (size > maxDisplayBytes) {\n return {\n kind: \"too-large\",\n size,\n maxDisplayBytes,\n downloading: download.requested && download.isLoading,\n downloadUrl: download.objectUrl,\n downloadName: download.fileName,\n };\n }\n if (streamed.isLoading) return { kind: \"streaming\", size };\n if (streamed.isError) {\n return { kind: \"stream-error\", size, message: streamed.errorMessage ?? \"stream failed\" };\n }\n if (streamed.content !== null) return { kind: \"streamed\", size, content: streamed.content };\n return { kind: \"loading\" };\n }\n\n if (inline.isError) {\n return { kind: \"error\", message: inline.errorMessage ?? \"unknown error\" };\n }\n\n if (inline.content !== null) return { kind: \"inline\", content: inline.content };\n return { kind: \"loading\" };\n}\n","/**\n * TextViewer — the default viewer for source/text files: fetches the UTF-8 preview via\n * `useFileRead` and renders it through `CodeView` (line gutter + Shiki highlighting). Registered\n * in `viewer-registry.ts` as the fallback for any file not claimed by a more specific viewer.\n *\n * Three size tiers (task-009 — raising `file_read_request`'s old 512 KiB ceiling):\n * 1. `size <= MAX_INLINE_FILE_READ_BYTES` (server-side, 5 MiB) — the `useFileRead` JSON round\n * trip above, unchanged.\n * 2. `MAX_INLINE_FILE_READ_BYTES < size <= MAX_DISPLAY_BYTES` (30 MiB) — transparently refetch via\n * the uncapped chunked binary download path (`use-file-text.ts`) and render the same\n * `CodeView`, with a muted note that the file was streamed rather than read inline.\n * 3. `size > MAX_DISPLAY_BYTES` — a terminal state: no render attempt, just the size, why, and a\n * download action (`BinaryFallbackViewer`'s existing on-demand download pattern).\n *\n * Which tier applies is the pure, DOM-free `selectTextViewerState` (`text-viewer-state.ts`) — this\n * component is a thin switch-render over it.\n */\n\nimport { useState } from \"react\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { useFileRead, FileTooLargeError } from \"@pi-studio-ui/hooks/use-file-read.js\";\nimport { useFileText } from \"@pi-studio-ui/hooks/use-file-text.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport { CodeView } from \"./CodeView.js\";\nimport { selectTextViewerState } from \"./text-viewer-state.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport styles from \"./FilePanel.module.css\";\nimport textStyles from \"./TextViewer.module.css\";\n\n/** Display ceiling above the inline cap. `CodeView`'s CodeMirror instance enables\n * `EditorView.lineWrapping`, which makes it measure line heights across the whole document —\n * well past interactive above this size, so files this large get a terminal download-only state\n * instead of an attempted render. Deliberately not configurable (task-009 notes). */\nexport const MAX_DISPLAY_BYTES = 30 * 1024 * 1024;\n\nfunction formatMegabytes(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function TextViewer({ path }: ViewerProps) {\n const inline = useFileRead(path);\n const tooLarge = inline.error instanceof FileTooLargeError ? inline.error : null;\n const displayable = tooLarge !== null && tooLarge.size <= MAX_DISPLAY_BYTES;\n\n const streamed = useFileText(path, displayable);\n const [downloadRequested, setDownloadRequested] = useState(false);\n const download = useFileDownload(path, downloadRequested);\n\n const state = selectTextViewerState({\n maxDisplayBytes: MAX_DISPLAY_BYTES,\n inline: {\n isLoading: inline.isLoading,\n isError: inline.isError,\n tooLarge: tooLarge ? { size: tooLarge.size } : null,\n errorMessage: inline.error instanceof Error ? inline.error.message : null,\n content: inline.data?.content ?? null,\n },\n streamed: {\n isLoading: streamed.isLoading,\n isError: streamed.isError,\n errorMessage: streamed.error instanceof Error ? streamed.error.message : null,\n content: streamed.data?.content ?? null,\n },\n download: {\n requested: downloadRequested,\n isLoading: download.isLoading,\n objectUrl: download.data?.objectUrl ?? null,\n fileName: download.data?.fileName ?? null,\n },\n });\n\n switch (state.kind) {\n case \"loading\":\n return (\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n );\n case \"inline\":\n return <CodeView path={path} content={state.content} />;\n case \"streaming\":\n return (\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Streaming {formatMegabytes(state.size)} file...\n </div>\n );\n case \"stream-error\":\n return <div className={styles.emptyState}>Error: {state.message}</div>;\n case \"streamed\":\n return (\n <div className={textStyles.wrap}>\n <div className={textStyles.note}>{formatMegabytes(state.size)} file streamed</div>\n <div className={textStyles.body}>\n <CodeView path={path} content={state.content} />\n </div>\n </div>\n );\n case \"too-large\": {\n const name = path.split(\"/\").pop() || path;\n return (\n <div className={styles.emptyState}>\n <div className={textStyles.tooLarge}>\n <div>\n {formatMegabytes(state.size)} — too large to display (display ceiling is{\" \"}\n {formatMegabytes(state.maxDisplayBytes)}).\n </div>\n {state.downloading ? (\n <Spinner size=\"sm\" />\n ) : state.downloadUrl ? (\n <a href={state.downloadUrl} download={state.downloadName || name}>\n <Button size=\"sm\">Save file</Button>\n </a>\n ) : (\n <Button size=\"sm\" onClick={() => setDownloadRequested(true)}>\n Download\n </Button>\n )}\n </div>\n </div>\n );\n }\n case \"error\":\n return <div className={styles.emptyState}>Error: {state.message}</div>;\n }\n}\n"],"names":["mergeFileTextState","input","enabled","download","decode","useFileText","path","useFileDownload","objectUrl","_a","useQuery","rpcKeys","selectTextViewerState","maxDisplayBytes","inline","streamed","size","MAX_DISPLAY_BYTES","formatMegabytes","bytes","TextViewer","useFileRead","tooLarge","FileTooLargeError","displayable","downloadRequested","setDownloadRequested","useState","state","_b","_c","_d","jsxs","styles","jsx","Spinner","CodeView","textStyles","name","Button"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{j as o}from"./vendor-react-p-LLqNcx.js";import{S as t}from"./index-DneBC_ta.js";import{u as a}from"./use-file-download-DsIqcxhB.js";import{p as e}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";const n="_wrap_1c9y4_1",m="_video_1c9y4_10",i={wrap:n,video:m};function _({path:s}){const r=a(s);return r.isLoading?o.jsxs("div",{className:e.emptyState,children:[o.jsx(t,{size:"sm"})," Loading video..."]}):r.isError?o.jsxs("div",{className:e.emptyState,children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?o.jsx("div",{className:i.wrap,children:o.jsx("video",{className:i.video,src:r.data.objectUrl,controls:!0})}):null}export{_ as VideoViewer};
2
- //# sourceMappingURL=VideoViewer-D4XJC0lT.js.map
1
+ import{j as o}from"./vendor-react-p-LLqNcx.js";import{S as t}from"./index-DulchPgH.js";import{u as a}from"./use-file-download-AaY3793H.js";import{p as e}from"./FilePanel.module-DoDNR5C2.js";import"./vendor-query-chxBrUuj.js";import"./vendor-B2Vnrid8.js";import"./vendor-markdown-ePgMI45n.js";import"./vendor-highlight-Cry4x-mr.js";import"./vendor-icons-CAhpOZSZ.js";import"./vendor-overlays-DWi-xJna.js";import"./vendor-dnd-CjSU1Kqk.js";const n="_wrap_1c9y4_1",m="_video_1c9y4_10",i={wrap:n,video:m};function _({path:s}){const r=a(s);return r.isLoading?o.jsxs("div",{className:e.emptyState,children:[o.jsx(t,{size:"sm"})," Loading video..."]}):r.isError?o.jsxs("div",{className:e.emptyState,children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?o.jsx("div",{className:i.wrap,children:o.jsx("video",{className:i.video,src:r.data.objectUrl,controls:!0})}):null}export{_ as VideoViewer};
2
+ //# sourceMappingURL=VideoViewer-Bv9fQkLE.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"VideoViewer-D4XJC0lT.js","sources":["../../../src/features/files/VideoViewer.tsx"],"sourcesContent":["/**\n * VideoViewer — renders a binary video file via the file-transfer binary download\n * (`useFileDownload`) into a native `<video>` element (POC_TO_APP_PLAN_UI.md §4.5 follow-up:\n * modular file preview).\n */\n\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport panelStyles from \"./FilePanel.module.css\";\nimport styles from \"./VideoViewer.module.css\";\n\nexport function VideoViewer({ path }: ViewerProps) {\n const query = useFileDownload(path);\n\n if (query.isLoading) {\n return (\n <div className={panelStyles.emptyState}>\n <Spinner size=\"sm\" /> Loading video...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={panelStyles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n {/* eslint-disable-next-line jsx-a11y/media-has-caption -- arbitrary local file, no caption track available */}\n <video className={styles.video} src={query.data.objectUrl} controls />\n </div>\n );\n}\n"],"names":["VideoViewer","path","query","useFileDownload","jsxs","panelStyles","jsx","Spinner","styles"],"mappings":"ofAYO,SAASA,EAAY,CAAE,KAAAC,GAAqB,CACjD,MAAMC,EAAQC,EAAgBF,CAAI,EAElC,OAAIC,EAAM,UAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAC1B,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,mBAAA,EACvB,EAGAL,EAAM,QAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAAY,SAAA,CAAA,UAC9BH,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,WAGR,MAAA,CAAI,UAAWM,EAAO,KAErB,eAAC,QAAA,CAAM,UAAWA,EAAO,MAAO,IAAKN,EAAM,KAAK,UAAW,SAAQ,GAAC,EACtE,EANsB,IAQ1B"}
1
+ {"version":3,"file":"VideoViewer-Bv9fQkLE.js","sources":["../../../src/features/files/VideoViewer.tsx"],"sourcesContent":["/**\n * VideoViewer — renders a binary video file via the file-transfer binary download\n * (`useFileDownload`) into a native `<video>` element (POC_TO_APP_PLAN_UI.md §4.5 follow-up:\n * modular file preview).\n */\n\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { useFileDownload } from \"@pi-studio-ui/hooks/use-file-download.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport panelStyles from \"./FilePanel.module.css\";\nimport styles from \"./VideoViewer.module.css\";\n\nexport function VideoViewer({ path }: ViewerProps) {\n const query = useFileDownload(path);\n\n if (query.isLoading) {\n return (\n <div className={panelStyles.emptyState}>\n <Spinner size=\"sm\" /> Loading video...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={panelStyles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n\n return (\n <div className={styles.wrap}>\n {/* eslint-disable-next-line jsx-a11y/media-has-caption -- arbitrary local file, no caption track available */}\n <video className={styles.video} src={query.data.objectUrl} controls />\n </div>\n );\n}\n"],"names":["VideoViewer","path","query","useFileDownload","jsxs","panelStyles","jsx","Spinner","styles"],"mappings":"ofAYO,SAASA,EAAY,CAAE,KAAAC,GAAqB,CACjD,MAAMC,EAAQC,EAAgBF,CAAI,EAElC,OAAIC,EAAM,UAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAC1B,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,mBAAA,EACvB,EAGAL,EAAM,QAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAY,WAAY,SAAA,CAAA,UAC9BH,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,WAGR,MAAA,CAAI,UAAWM,EAAO,KAErB,eAAC,QAAA,CAAM,UAAWA,EAAO,MAAO,IAAKN,EAAM,KAAK,UAAW,SAAQ,GAAC,EACtE,EANsB,IAQ1B"}