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

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 (36) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/web/assets/{BinaryFallbackViewer-Diavog5U.js → BinaryFallbackViewer-DN2oT6N7.js} +2 -2
  3. package/dist/web/assets/{BinaryFallbackViewer-Diavog5U.js.map → BinaryFallbackViewer-DN2oT6N7.js.map} +1 -1
  4. package/dist/web/assets/{ChatPanel-sp_aMtby.js → ChatPanel-YUfYdTdZ.js} +2 -2
  5. package/dist/web/assets/{ChatPanel-sp_aMtby.js.map → ChatPanel-YUfYdTdZ.js.map} +1 -1
  6. package/dist/web/assets/{CodeView-BO9eNSWw.js → CodeView-DwUKpXwU.js} +2 -2
  7. package/dist/web/assets/{CodeView-BO9eNSWw.js.map → CodeView-DwUKpXwU.js.map} +1 -1
  8. package/dist/web/assets/FilePanel-DCZ6Vntr.js +2 -0
  9. package/dist/web/assets/FilePanel-DCZ6Vntr.js.map +1 -0
  10. package/dist/web/assets/{ImageViewer-DA1VjBRz.js → ImageViewer-BKflylHn.js} +2 -2
  11. package/dist/web/assets/{ImageViewer-DA1VjBRz.js.map → ImageViewer-BKflylHn.js.map} +1 -1
  12. package/dist/web/assets/{MarkdownFileViewer-Co5iIbJn.js → MarkdownFileViewer-BDmo3la-.js} +2 -2
  13. package/dist/web/assets/{MarkdownFileViewer-Co5iIbJn.js.map → MarkdownFileViewer-BDmo3la-.js.map} +1 -1
  14. package/dist/web/assets/MoleculeViewerPanel-BZA00vSx.js +2 -0
  15. package/dist/web/assets/MoleculeViewerPanel-BZA00vSx.js.map +1 -0
  16. package/dist/web/assets/{TerminalPanel-BfOpqaj0.js → TerminalPanel-qx_HdM7y.js} +2 -2
  17. package/dist/web/assets/{TerminalPanel-BfOpqaj0.js.map → TerminalPanel-qx_HdM7y.js.map} +1 -1
  18. package/dist/web/assets/TextViewer-CDXNfUrU.js +2 -0
  19. package/dist/web/assets/TextViewer-CDXNfUrU.js.map +1 -0
  20. package/dist/web/assets/{VideoViewer-SeMcovVF.js → VideoViewer-D4XJC0lT.js} +2 -2
  21. package/dist/web/assets/{VideoViewer-SeMcovVF.js.map → VideoViewer-D4XJC0lT.js.map} +1 -1
  22. package/dist/web/assets/{index-CeQxaU3V.js → index-DneBC_ta.js} +3 -3
  23. package/dist/web/assets/index-DneBC_ta.js.map +1 -0
  24. package/dist/web/assets/{use-file-download-BDc3Flim.js → use-file-download-DsIqcxhB.js} +2 -2
  25. package/dist/web/assets/{use-file-download-BDc3Flim.js.map → use-file-download-DsIqcxhB.js.map} +1 -1
  26. package/dist/web/assets/use-file-watch-ld_2lbTl.js +2 -0
  27. package/dist/web/assets/use-file-watch-ld_2lbTl.js.map +1 -0
  28. package/dist/web/index.html +1 -1
  29. package/package.json +3 -3
  30. package/dist/web/assets/FilePanel-Dla22Uov.js +0 -2
  31. package/dist/web/assets/FilePanel-Dla22Uov.js.map +0 -1
  32. package/dist/web/assets/MoleculeViewerPanel-BTsiMK_L.js +0 -2
  33. package/dist/web/assets/MoleculeViewerPanel-BTsiMK_L.js.map +0 -1
  34. package/dist/web/assets/TextViewer-C76HR2kP.js +0 -2
  35. package/dist/web/assets/TextViewer-C76HR2kP.js.map +0 -1
  36. package/dist/web/assets/index-CeQxaU3V.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as a}from"./vendor-react-p-LLqNcx.js";import{u as f}from"./vendor-query-chxBrUuj.js";import{a as s,g as m,r as y}from"./index-CeQxaU3V.js";function p(o,u=!0){var i;const l=s(e=>e.client),n=s(e=>e.daemon),c=f({queryKey:y.fileDownload(o),queryFn:async()=>{if(!l||!n)throw new Error("not connected");const e=await m(n).download(o),t=new Blob([Uint8Array.from(e.bytes)],{type:e.mimeType||"application/octet-stream"});return{objectUrl:URL.createObjectURL(t),mimeType:e.mimeType,fileName:e.fileName}},enabled:!!l&&!!n&&!!o&&u,staleTime:1/0,gcTime:0}),r=a.useRef(null);return a.useEffect(()=>{var t;const e=((t=c.data)==null?void 0:t.objectUrl)??null;e&&r.current&&r.current!==e&&URL.revokeObjectURL(r.current),r.current=e},[(i=c.data)==null?void 0:i.objectUrl]),a.useEffect(()=>()=>{r.current&&URL.revokeObjectURL(r.current)},[]),c}export{p as u};
2
- //# sourceMappingURL=use-file-download-BDc3Flim.js.map
1
+ import{r as a}from"./vendor-react-p-LLqNcx.js";import{u as f}from"./vendor-query-chxBrUuj.js";import{a as s,g as m,r as y}from"./index-DneBC_ta.js";function p(o,u=!0){var i;const l=s(e=>e.client),n=s(e=>e.daemon),c=f({queryKey:y.fileDownload(o),queryFn:async()=>{if(!l||!n)throw new Error("not connected");const e=await m(n).download(o),t=new Blob([Uint8Array.from(e.bytes)],{type:e.mimeType||"application/octet-stream"});return{objectUrl:URL.createObjectURL(t),mimeType:e.mimeType,fileName:e.fileName}},enabled:!!l&&!!n&&!!o&&u,staleTime:1/0,gcTime:0}),r=a.useRef(null);return a.useEffect(()=>{var t;const e=((t=c.data)==null?void 0:t.objectUrl)??null;e&&r.current&&r.current!==e&&URL.revokeObjectURL(r.current),r.current=e},[(i=c.data)==null?void 0:i.objectUrl]),a.useEffect(()=>()=>{r.current&&URL.revokeObjectURL(r.current)},[]),c}export{p as u};
2
+ //# sourceMappingURL=use-file-download-DsIqcxhB.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-file-download-BDc3Flim.js","sources":["../../../src/hooks/use-file-download.ts"],"sourcesContent":["/**\n * `use-file-download` — TanStack Query wrapper over the binary file-transfer download path\n * (`file_download_token_request` → `file_download_request` → chunked `Begin`/`Chunk`/`End`\n * binary frames), for viewers that need raw bytes rather than the UTF-8 preview\n * `file_read_request` returns (images, video — POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular\n * file preview). Resolves to an object URL, revoked once superseded by a new one or once this\n * consumer unmounts.\n */\n\nimport { useEffect, useRef } from \"react\";\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\";\nimport { transferFor } from \"./file-transfer-instance.js\";\n\nexport interface FileDownload {\n objectUrl: string;\n mimeType?: string;\n fileName?: string;\n}\n\nexport function useFileDownload(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n const daemon = useConnectionStore((s) => s.daemon);\n\n const query = useQuery({\n queryKey: rpcKeys.fileDownload(path),\n queryFn: async (): Promise<FileDownload> => {\n if (!client || !daemon) throw new Error(\"not connected\");\n const file = await transferFor(daemon).download(path);\n // `Blob`'s DOM typings require an `ArrayBuffer`-backed view; the assembled `Uint8Array`'s\n // `ArrayBufferLike` backing (a decode-time implementation detail) doesn't satisfy that on\n // its own, so copy into a plain `ArrayBuffer`-backed `Uint8Array` first.\n const blob = new Blob([Uint8Array.from(file.bytes)], {\n type: file.mimeType || \"application/octet-stream\",\n });\n return { objectUrl: URL.createObjectURL(blob), mimeType: file.mimeType, fileName: file.fileName };\n },\n enabled: Boolean(client) && Boolean(daemon) && Boolean(path) && enabled,\n // Object URLs are process-local, non-serializable browser resources — never persist them or\n // silently refetch behind this hook's back (that would leak the previous blob's URL).\n staleTime: Infinity,\n gcTime: 0,\n });\n\n // Revoke the previous object URL once a new one supersedes it, and the current one on unmount\n // — TanStack Query's cache has no notion that this value owns a browser resource to release.\n const urlRef = useRef<string | null>(null);\n useEffect(() => {\n const nextUrl = query.data?.objectUrl ?? null;\n if (nextUrl && urlRef.current && urlRef.current !== nextUrl) {\n URL.revokeObjectURL(urlRef.current);\n }\n urlRef.current = nextUrl;\n }, [query.data?.objectUrl]);\n\n useEffect(() => {\n return () => {\n if (urlRef.current) URL.revokeObjectURL(urlRef.current);\n };\n }, []);\n\n return query;\n}\n"],"names":["useFileDownload","path","enabled","client","useConnectionStore","s","daemon","query","useQuery","rpcKeys","file","transferFor","blob","urlRef","useRef","useEffect","nextUrl","_a"],"mappings":"oJAqBO,SAASA,EAAgBC,EAAcC,EAAU,GAAM,OAC5D,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAC3CC,EAASF,EAAoBC,GAAMA,EAAE,MAAM,EAE3CE,EAAQC,EAAS,CACrB,SAAUC,EAAQ,aAAaR,CAAI,EACnC,QAAS,SAAmC,CAC1C,GAAI,CAACE,GAAU,CAACG,EAAQ,MAAM,IAAI,MAAM,eAAe,EACvD,MAAMI,EAAO,MAAMC,EAAYL,CAAM,EAAE,SAASL,CAAI,EAI9CW,EAAO,IAAI,KAAK,CAAC,WAAW,KAAKF,EAAK,KAAK,CAAC,EAAG,CACnD,KAAMA,EAAK,UAAY,0BAAA,CACxB,EACD,MAAO,CAAE,UAAW,IAAI,gBAAgBE,CAAI,EAAG,SAAUF,EAAK,SAAU,SAAUA,EAAK,QAAA,CACzF,EACA,QAAS,EAAQP,GAAW,EAAQG,GAAW,EAAQL,GAASC,EAGhE,UAAW,IACX,OAAQ,CAAA,CACT,EAIKW,EAASC,EAAAA,OAAsB,IAAI,EACzCC,OAAAA,EAAAA,UAAU,IAAM,OACd,MAAMC,IAAUC,EAAAV,EAAM,OAAN,YAAAU,EAAY,YAAa,KACrCD,GAAWH,EAAO,SAAWA,EAAO,UAAYG,GAClD,IAAI,gBAAgBH,EAAO,OAAO,EAEpCA,EAAO,QAAUG,CACnB,EAAG,EAACC,EAAAV,EAAM,OAAN,YAAAU,EAAY,SAAS,CAAC,EAE1BF,EAAAA,UAAU,IACD,IAAM,CACPF,EAAO,SAAS,IAAI,gBAAgBA,EAAO,OAAO,CACxD,EACC,CAAA,CAAE,EAEEN,CACT"}
1
+ {"version":3,"file":"use-file-download-DsIqcxhB.js","sources":["../../../src/hooks/use-file-download.ts"],"sourcesContent":["/**\n * `use-file-download` — TanStack Query wrapper over the binary file-transfer download path\n * (`file_download_token_request` → `file_download_request` → chunked `Begin`/`Chunk`/`End`\n * binary frames), for viewers that need raw bytes rather than the UTF-8 preview\n * `file_read_request` returns (images, video — POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular\n * file preview). Resolves to an object URL, revoked once superseded by a new one or once this\n * consumer unmounts.\n */\n\nimport { useEffect, useRef } from \"react\";\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\";\nimport { transferFor } from \"./file-transfer-instance.js\";\n\nexport interface FileDownload {\n objectUrl: string;\n mimeType?: string;\n fileName?: string;\n}\n\nexport function useFileDownload(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n const daemon = useConnectionStore((s) => s.daemon);\n\n const query = useQuery({\n queryKey: rpcKeys.fileDownload(path),\n queryFn: async (): Promise<FileDownload> => {\n if (!client || !daemon) throw new Error(\"not connected\");\n const file = await transferFor(daemon).download(path);\n // `Blob`'s DOM typings require an `ArrayBuffer`-backed view; the assembled `Uint8Array`'s\n // `ArrayBufferLike` backing (a decode-time implementation detail) doesn't satisfy that on\n // its own, so copy into a plain `ArrayBuffer`-backed `Uint8Array` first.\n const blob = new Blob([Uint8Array.from(file.bytes)], {\n type: file.mimeType || \"application/octet-stream\",\n });\n return { objectUrl: URL.createObjectURL(blob), mimeType: file.mimeType, fileName: file.fileName };\n },\n enabled: Boolean(client) && Boolean(daemon) && Boolean(path) && enabled,\n // Object URLs are process-local, non-serializable browser resources — never persist them or\n // silently refetch behind this hook's back (that would leak the previous blob's URL).\n staleTime: Infinity,\n gcTime: 0,\n });\n\n // Revoke the previous object URL once a new one supersedes it, and the current one on unmount\n // — TanStack Query's cache has no notion that this value owns a browser resource to release.\n const urlRef = useRef<string | null>(null);\n useEffect(() => {\n const nextUrl = query.data?.objectUrl ?? null;\n if (nextUrl && urlRef.current && urlRef.current !== nextUrl) {\n URL.revokeObjectURL(urlRef.current);\n }\n urlRef.current = nextUrl;\n }, [query.data?.objectUrl]);\n\n useEffect(() => {\n return () => {\n if (urlRef.current) URL.revokeObjectURL(urlRef.current);\n };\n }, []);\n\n return query;\n}\n"],"names":["useFileDownload","path","enabled","client","useConnectionStore","s","daemon","query","useQuery","rpcKeys","file","transferFor","blob","urlRef","useRef","useEffect","nextUrl","_a"],"mappings":"oJAqBO,SAASA,EAAgBC,EAAcC,EAAU,GAAM,OAC5D,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAC3CC,EAASF,EAAoBC,GAAMA,EAAE,MAAM,EAE3CE,EAAQC,EAAS,CACrB,SAAUC,EAAQ,aAAaR,CAAI,EACnC,QAAS,SAAmC,CAC1C,GAAI,CAACE,GAAU,CAACG,EAAQ,MAAM,IAAI,MAAM,eAAe,EACvD,MAAMI,EAAO,MAAMC,EAAYL,CAAM,EAAE,SAASL,CAAI,EAI9CW,EAAO,IAAI,KAAK,CAAC,WAAW,KAAKF,EAAK,KAAK,CAAC,EAAG,CACnD,KAAMA,EAAK,UAAY,0BAAA,CACxB,EACD,MAAO,CAAE,UAAW,IAAI,gBAAgBE,CAAI,EAAG,SAAUF,EAAK,SAAU,SAAUA,EAAK,QAAA,CACzF,EACA,QAAS,EAAQP,GAAW,EAAQG,GAAW,EAAQL,GAASC,EAGhE,UAAW,IACX,OAAQ,CAAA,CACT,EAIKW,EAASC,EAAAA,OAAsB,IAAI,EACzCC,OAAAA,EAAAA,UAAU,IAAM,OACd,MAAMC,IAAUC,EAAAV,EAAM,OAAN,YAAAU,EAAY,YAAa,KACrCD,GAAWH,EAAO,SAAWA,EAAO,UAAYG,GAClD,IAAI,gBAAgBH,EAAO,OAAO,EAEpCA,EAAO,QAAUG,CACnB,EAAG,EAACC,EAAAV,EAAM,OAAN,YAAAU,EAAY,SAAS,CAAC,EAE1BF,EAAAA,UAAU,IACD,IAAM,CACPF,EAAO,SAAS,IAAI,gBAAgBA,EAAO,OAAO,CACxD,EACC,CAAA,CAAE,EAEEN,CACT"}
@@ -0,0 +1,2 @@
1
+ import{r as a}from"./vendor-react-p-LLqNcx.js";import{a as w}from"./index-DneBC_ta.js";function b(e){return typeof e=="object"&&e!==null&&"type"in e&&e.type==="file_changed"&&"path"in e&&typeof e.path=="string"}const l=new WeakMap;function p(e,t,i){let s=l.get(e);s||(s=new Map,l.set(e,s));let o=s.get(t);if(!o){const r={count:0,resolvedPath:null,listeners:new Set,dispose:()=>{}},f=e.connection.onSessionMessage(c=>{const n=c;if(!b(n)||n.path!==t&&n.path!==r.resolvedPath)return;const d=Date.now();for(const h of r.listeners)h(d)});e.connection.request("file_watch_subscribe",{path:t}).then(c=>{const n=c;if((n==null?void 0:n.ok)===!1){console.warn(`watchFile: ${n.error??"subscribe failed"} for "${t}"`);return}typeof(n==null?void 0:n.path)=="string"&&(r.resolvedPath=n.path)}).catch(()=>{}),r.dispose=()=>{f(),e.connection.request("file_watch_unsubscribe",{path:t}).catch(()=>{})},o=r,s.set(t,o)}o.count++,o.listeners.add(i);let u=!1;return()=>{if(u)return;u=!0;const r=s.get(t);r&&(r.listeners.delete(i),r.count--,r.count===0&&(s.delete(t),r.dispose()))}}function v(e){const t=w(o=>o.client),[i,s]=a.useState(null);return a.useEffect(()=>{if(!(!t||!e))return p(t,e,s)},[t,e]),{changedAt:i}}export{v as u};
2
+ //# sourceMappingURL=use-file-watch-ld_2lbTl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-file-watch-ld_2lbTl.js","sources":["../../../src/hooks/use-file-watch.ts"],"sourcesContent":["/**\n * `use-file-watch` — subscribes to live daemon `file_changed` pushes for a path (task-006's\n * `FileWatchService`), structurally mirroring `use-checkout-status.ts`: subscribe on mount/path\n * change via `file_watch_subscribe`, route matching pushes through `onSessionMessage`, and on\n * cleanup detach the message handler AND send `file_watch_unsubscribe` for the path being left —\n * `use-checkout-status.ts`'s own header comment calls out that leaving either half undone is the\n * leak this pattern exists to avoid.\n *\n * The actual subscribe/route/cleanup wiring lives in `watchFile`, a plain function with no React\n * dependency (mirrors `molecule-source.ts`/`molecule-reload.ts`'s \"extract the testable core\"\n * convention) — this repo has no DOM test environment set up, so it is the seam\n * `use-file-watch.test.ts` exercises directly with a fake client rather than mounting the hook.\n *\n * A `null` path (an empty \"+\"-menu molecule tab, or the absence of a client) subscribes nothing.\n *\n * `watchFile` ref-counts subscriptions per `(client, path)` in a module-level registry (mirrors\n * `file-transfer-instance.ts`'s `WeakMap<DaemonClient, …>` shared-instance convention): more than\n * one component can watch the same file (a file tab and a diff tab on the same path) and share\n * ONE daemon watch, rather than the second `file_watch_unsubscribe` killing it for both — the\n * daemon's `SessionSubscriptions.add` disposes any existing entry for the same resolved-path key.\n * The entry also remembers the RESOLVED path echoed back by `file_watch_subscribe_response.path`\n * (the daemon expands a leading `~` server-side before pushing `file_changed`), and matches a\n * push against either the requested spelling or that resolved path.\n */\n\nimport { useEffect, useState } from \"react\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\n\ninterface FileChangedMessage {\n type: \"file_changed\";\n path: string;\n}\n\nfunction isFileChangedMessage(msg: unknown): msg is FileChangedMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"file_changed\" &&\n \"path\" in msg &&\n typeof msg.path === \"string\"\n );\n}\n\ninterface FileWatchSubscribeResponse {\n ok?: boolean;\n error?: string;\n path?: string;\n}\n\ninterface WatchEntry {\n count: number;\n /** Resolved path echoed by `file_watch_subscribe_response.path`; `null` until it arrives. */\n resolvedPath: string | null;\n listeners: Set<(changedAt: number) => void>;\n dispose: () => void;\n}\n\nconst watchesByClient = new WeakMap<Pick<PiStudioClient, \"connection\">, Map<string, WatchEntry>>();\n\n/** Subscribes `path` on `client.connection`, calls `onChanged` for every matching `file_changed`\n * push, and returns a cleanup function that detaches the handler AND sends\n * `file_watch_unsubscribe` for `path` once the LAST subscriber for that `(client, path)` pair\n * releases it. Framework-agnostic on purpose — see module header. */\nexport function watchFile(\n client: Pick<PiStudioClient, \"connection\">,\n path: string,\n onChanged: (changedAt: number) => void,\n): () => void {\n let entries = watchesByClient.get(client);\n if (!entries) {\n entries = new Map<string, WatchEntry>();\n watchesByClient.set(client, entries);\n }\n\n let entry = entries.get(path);\n if (!entry) {\n const created: WatchEntry = {\n count: 0,\n resolvedPath: null,\n listeners: new Set(),\n dispose: () => {},\n };\n\n const unsubscribeMessages = client.connection.onSessionMessage((msg) => {\n const raw: unknown = msg;\n if (!isFileChangedMessage(raw)) return;\n if (raw.path !== path && raw.path !== created.resolvedPath) return;\n const changedAt = Date.now();\n for (const listener of created.listeners) listener(changedAt);\n });\n\n void client.connection\n .request(\"file_watch_subscribe\", { path })\n .then((res) => {\n const response = res as FileWatchSubscribeResponse;\n if (response?.ok === false) {\n console.warn(`watchFile: ${response.error ?? \"subscribe failed\"} for \"${path}\"`);\n return;\n }\n if (typeof response?.path === \"string\") {\n created.resolvedPath = response.path;\n }\n })\n .catch(() => {});\n\n created.dispose = () => {\n unsubscribeMessages();\n void client.connection.request(\"file_watch_unsubscribe\", { path }).catch(() => {});\n };\n\n entry = created;\n entries.set(path, entry);\n }\n\n entry.count++;\n entry.listeners.add(onChanged);\n\n let released = false;\n return () => {\n if (released) return; // Idempotent: a second call must never double-release.\n released = true;\n const current = entries!.get(path);\n if (!current) return;\n current.listeners.delete(onChanged);\n current.count--;\n if (current.count === 0) {\n entries!.delete(path);\n current.dispose();\n }\n };\n}\n\nexport interface UseFileWatchResult {\n /** Monotonic timestamp of the last `file_changed` push for this path; `null` if none yet. */\n changedAt: number | null;\n}\n\nexport function useFileWatch(path: string | null): UseFileWatchResult {\n const client = useConnectionStore((s) => s.client);\n const [changedAt, setChangedAt] = useState<number | null>(null);\n\n useEffect(() => {\n if (!client || !path) return;\n return watchFile(client, path, setChangedAt);\n }, [client, path]);\n\n return { changedAt };\n}\n"],"names":["isFileChangedMessage","msg","watchesByClient","watchFile","client","path","onChanged","entries","entry","created","unsubscribeMessages","raw","changedAt","listener","res","response","released","current","useFileWatch","useConnectionStore","s","setChangedAt","useState","useEffect"],"mappings":"uFAkCA,SAASA,EAAqBC,EAAyC,CACrE,OACE,OAAOA,GAAQ,UACfA,IAAQ,MACR,SAAUA,GACVA,EAAI,OAAS,gBACb,SAAUA,GACV,OAAOA,EAAI,MAAS,QAExB,CAgBA,MAAMC,MAAsB,QAMrB,SAASC,EACdC,EACAC,EACAC,EACY,CACZ,IAAIC,EAAUL,EAAgB,IAAIE,CAAM,EACnCG,IACHA,MAAc,IACdL,EAAgB,IAAIE,EAAQG,CAAO,GAGrC,IAAIC,EAAQD,EAAQ,IAAIF,CAAI,EAC5B,GAAI,CAACG,EAAO,CACV,MAAMC,EAAsB,CAC1B,MAAO,EACP,aAAc,KACd,cAAe,IACf,QAAS,IAAM,CAAC,CAAA,EAGZC,EAAsBN,EAAO,WAAW,iBAAkBH,GAAQ,CACtE,MAAMU,EAAeV,EAErB,GADI,CAACD,EAAqBW,CAAG,GACzBA,EAAI,OAASN,GAAQM,EAAI,OAASF,EAAQ,aAAc,OAC5D,MAAMG,EAAY,KAAK,IAAA,EACvB,UAAWC,KAAYJ,EAAQ,UAAWI,EAASD,CAAS,CAC9D,CAAC,EAEIR,EAAO,WACT,QAAQ,uBAAwB,CAAE,KAAAC,EAAM,EACxC,KAAMS,GAAQ,CACb,MAAMC,EAAWD,EACjB,IAAIC,GAAA,YAAAA,EAAU,MAAO,GAAO,CAC1B,QAAQ,KAAK,cAAcA,EAAS,OAAS,kBAAkB,SAASV,CAAI,GAAG,EAC/E,MACF,CACI,OAAOU,GAAA,YAAAA,EAAU,OAAS,WAC5BN,EAAQ,aAAeM,EAAS,KAEpC,CAAC,EACA,MAAM,IAAM,CAAC,CAAC,EAEjBN,EAAQ,QAAU,IAAM,CACtBC,EAAA,EACKN,EAAO,WAAW,QAAQ,yBAA0B,CAAE,KAAAC,CAAA,CAAM,EAAE,MAAM,IAAM,CAAC,CAAC,CACnF,EAEAG,EAAQC,EACRF,EAAQ,IAAIF,EAAMG,CAAK,CACzB,CAEAA,EAAM,QACNA,EAAM,UAAU,IAAIF,CAAS,EAE7B,IAAIU,EAAW,GACf,MAAO,IAAM,CACX,GAAIA,EAAU,OACdA,EAAW,GACX,MAAMC,EAAUV,EAAS,IAAIF,CAAI,EAC5BY,IACLA,EAAQ,UAAU,OAAOX,CAAS,EAClCW,EAAQ,QACJA,EAAQ,QAAU,IACpBV,EAAS,OAAOF,CAAI,EACpBY,EAAQ,QAAA,GAEZ,CACF,CAOO,SAASC,EAAab,EAAyC,CACpE,MAAMD,EAASe,EAAoBC,GAAMA,EAAE,MAAM,EAC3C,CAACR,EAAWS,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAE9DC,OAAAA,EAAAA,UAAU,IAAM,CACd,GAAI,GAACnB,GAAU,CAACC,GAChB,OAAOF,EAAUC,EAAQC,EAAMgB,CAAY,CAC7C,EAAG,CAACjB,EAAQC,CAAI,CAAC,EAEV,CAAE,UAAAO,CAAA,CACX"}
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Pi-Studio</title>
7
- <script type="module" crossorigin src="/assets/index-CeQxaU3V.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-DneBC_ta.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-p-LLqNcx.js">
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-query-chxBrUuj.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-markdown-ePgMI45n.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@av-pi-studio/web-client",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -14,8 +14,8 @@
14
14
  "clean": "rm -rf dist *.tsbuildinfo"
15
15
  },
16
16
  "dependencies": {
17
- "@av-pi-studio/client": "^0.0.44",
18
- "@av-pi-studio/protocol": "^0.0.44",
17
+ "@av-pi-studio/client": "^0.0.45",
18
+ "@av-pi-studio/protocol": "^0.0.45",
19
19
  "@codemirror/language": "^6.12.4",
20
20
  "@codemirror/language-data": "^6.5.2",
21
21
  "@codemirror/state": "^6.7.1",
@@ -1,2 +0,0 @@
1
- import{r as l,j as e}from"./vendor-react-p-LLqNcx.js";import{C as y}from"./vendor-B2Vnrid8.js";import{a as g,r as v,d as w,B as m,V as N,S as p}from"./index-CeQxaU3V.js";import{u as D}from"./vendor-query-chxBrUuj.js";import{D as E}from"./DiffView-CUxhzW_O.js";import{p as i}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 S(s,c,a,n=!0){const r=g(t=>t.client);return D({queryKey:v.fileDiff(s,c,a),queryFn:async()=>{if(!r)throw new Error("not connected");return{patch:(await r.connection.request("file_diff_request",{path:s,cwd:c,staged:a})).patch??""}},enabled:!!r&&!!s&&!!c&&n})}function u(s){return"path"in s}function k(s){return"staged"in s}function I({tab:s}){var f;const c=s.kind==="diff",a=s.data,n=u(a)?a.path:"",r=u(a)&&k(a)?a.staged:!1,[t,d]=l.useState(c?"diff":"file"),h=s.workspaceCwd||"~",o=S(n,h,r,t==="diff"),x=w(n),j=N[x];return e.jsxs("div",{className:i.panel,children:[e.jsxs("div",{className:i.header,children:[e.jsx("span",{className:i.path,children:n}),e.jsx("span",{className:i.size,children:t==="diff"?r?"staged":"unstaged":null}),e.jsxs("div",{className:i.viewToggle,children:[e.jsx(m,{size:"xs",variant:t==="file"?"default":"ghost",onClick:()=>d("file"),children:"File"}),e.jsx(m,{size:"xs",variant:t==="diff"?"default":"ghost",onClick:()=>d("diff"),children:"Diff"})]})]}),e.jsx("div",{className:y(i.body,"file-body"),children:t==="file"?e.jsx(l.Suspense,{fallback:e.jsxs("div",{className:i.emptyState,children:[e.jsx(p,{size:"sm"})," Loading..."]}),children:e.jsx(j,{path:n})}):o.isLoading?e.jsxs("div",{className:i.emptyState,children:[e.jsx(p,{size:"sm"})," Loading diff..."]}):o.isError?e.jsxs("div",{className:i.emptyState,children:["Error: ",o.error instanceof Error?o.error.message:"unknown error"]}):(f=o.data)!=null&&f.patch?e.jsx(E,{patch:o.data.patch}):e.jsx("div",{className:i.diffEmpty,children:"No changes (file matches HEAD)"})})]})}export{I as FilePanel};
2
- //# sourceMappingURL=FilePanel-Dla22Uov.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FilePanel-Dla22Uov.js","sources":["../../../src/hooks/use-file-diff.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 * 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\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 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\n const diffQuery = useFileDiff(path, cwd, staged, viewMode === \"diff\");\n\n const viewerKind = detectViewerKind(path);\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","hasPath","data","isDiffTabData","FilePanel","tab","isDiffTab","viewMode","setViewMode","useState","diffQuery","viewerKind","detectViewerKind","Viewer","VIEWER_BY_KIND","jsxs","styles","jsx","Button","clsx","Suspense","Spinner","_a","DiffView"],"mappings":"8eAiBO,SAASA,EAAYC,EAAcC,EAAaC,EAAiBC,EAAU,GAAM,CACtF,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAEjD,OAAOC,EAAS,CACd,SAAUC,EAAQ,SAASR,EAAMC,EAAKC,CAAM,EAC5C,QAAS,SAAqC,CAC5C,GAAI,CAACE,EAAQ,MAAM,IAAI,MAAM,eAAe,EAM5C,MAAO,CAAE,OALQ,MAAMA,EAAO,WAAW,QAA6B,oBAAqB,CACzF,KAAAJ,EACA,IAAAC,EACA,OAAAC,CAAA,CACD,GACwB,OAAS,EAAA,CACpC,EACA,QAAS,EAAQE,GAAW,EAAQJ,GAAS,EAAQC,GAAQE,CAAA,CAC9D,CACH,CCRA,SAASM,EAAQC,EAAsD,CACrE,MAAO,SAAUA,CACnB,CAEA,SAASC,EAAcD,EAAsD,CAC3E,MAAO,WAAYA,CACrB,CAMO,SAASE,EAAU,CAAE,IAAAC,GAAuB,OACjD,MAAMC,EAAYD,EAAI,OAAS,OACzBH,EAAOG,EAAI,KACXb,EAAOS,EAAQC,CAAI,EAAIA,EAAK,KAAO,GACnCR,EAASO,EAAQC,CAAI,GAAKC,EAAcD,CAAI,EAAIA,EAAK,OAAS,GAE9D,CAACK,EAAUC,CAAW,EAAIC,EAAAA,SAAmBH,EAAY,OAAS,MAAM,EACxEb,EAAMY,EAAI,cAAgB,IAE1BK,EAAYnB,EAAYC,EAAMC,EAAKC,EAAQa,IAAa,MAAM,EAE9DI,EAAaC,EAAiBpB,CAAI,EAClCqB,EAASC,EAAeH,CAAU,EAExC,OACEI,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,MACrB,SAAA,CAAAD,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,OACrB,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAWD,EAAO,KAAO,SAAAxB,EAAK,EACpCyB,EAAAA,IAAC,OAAA,CAAK,UAAWD,EAAO,KAAO,aAAa,OAAUtB,EAAS,SAAW,WAAc,IAAA,CAAK,EAC7FqB,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,WACrB,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAK,KACL,QAASX,IAAa,OAAS,UAAY,QAC3C,QAAS,IAAMC,EAAY,MAAM,EAClC,SAAA,MAAA,CAAA,EAGDS,EAAAA,IAACC,EAAA,CACC,KAAK,KACL,QAASX,IAAa,OAAS,UAAY,QAC3C,QAAS,IAAMC,EAAY,MAAM,EAClC,SAAA,MAAA,CAAA,CAED,CAAA,CACF,CAAA,EACF,QACC,MAAA,CAAI,UAAWW,EAAKH,EAAO,KAAM,WAAW,EAC1C,SAAAT,IAAa,aACXa,EAAAA,SAAA,CAAS,gBAAW,MAAA,CAAI,UAAWJ,EAAO,WAAY,SAAA,CAAAC,EAAAA,IAACI,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,CAAA,CAAW,EACrF,SAAAJ,EAAAA,IAACJ,EAAA,CAAO,KAAArB,CAAA,CAAY,EACtB,EACEkB,EAAU,UACZK,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,WACrB,SAAA,CAAAC,EAAAA,IAACI,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,kBAAA,EACvB,EACEX,EAAU,eACX,MAAA,CAAI,UAAWM,EAAO,WAAY,SAAA,CAAA,UACzBN,EAAU,iBAAiB,MAAQA,EAAU,MAAM,QAAU,eAAA,EACvE,GACEY,EAAAZ,EAAU,OAAV,MAAAY,EAAgB,MAClBL,EAAAA,IAACM,GAAS,MAAOb,EAAU,KAAK,KAAA,CAAO,EAEvCO,EAAAA,IAAC,MAAA,CAAI,UAAWD,EAAO,UAAW,0CAA8B,CAAA,CAEpE,CAAA,EACF,CAEJ"}
@@ -1,2 +0,0 @@
1
- import{r as n,j as a}from"./vendor-react-p-LLqNcx.js";import{a as g,S as j,c as x}from"./index-CeQxaU3V.js";import{i as S}from"./vendor-molviewer-PwwY6jQU.js";import{S as E}from"./StatusBadge-D5M084rB.js";import{u as y}from"./use-file-download-BDc3Flim.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 M(e){return typeof e=="object"&&e!==null&&"type"in e&&e.type==="file_changed"&&"path"in e&&typeof e.path=="string"}function R(e,r,s){let o=!1;e.connection.request("file_watch_subscribe",{path:r}).catch(()=>{});const t=e.connection.onSessionMessage(u=>{if(o)return;const l=u;M(l)&&l.path===r&&s(Date.now())});return()=>{o||(o=!0,t(),e.connection.request("file_watch_unsubscribe",{path:r}).catch(()=>{}))}}function A(e){const r=g(t=>t.client),[s,o]=n.useState(null);return n.useEffect(()=>{if(!(!r||!e))return R(r,e,o)},[r,e]),{changedAt:s}}function N(e,r){return!e||!r?null:{url:r,name:e.split("/").pop()||e}}function V({changedAt:e,lastAppliedAt:r,modified:s}){return!(e===null||s||e===r)}const B={"--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)"},L="_wrap_auscj_1",F="_molViewer_auscj_8",H="_emptyState_auscj_12",T="_staleBadge_auscj_21",c={wrap:L,molViewer:F,emptyState:H,staleBadge:T};function I({path:e,isActive:r,onModifiedChange:s}){var w;const o=y(e??"",!!e),{changedAt:t}=A(e),u=n.useRef(null),l=n.useRef(!1),[p,v]=n.useState(null),[d,h]=n.useState(!1),f=n.useRef(null),_=N(e,(w=o.data)==null?void 0:w.objectUrl);n.useEffect(()=>{V({changedAt:t,lastAppliedAt:f.current,modified:d})&&(f.current=t,o.refetch())},[t,d,o.refetch]);const b=t!==null&&t!==f.current&&d,m=(o.isError?o.error instanceof Error?o.error.message:"download failed":null)??(p==null?void 0:p.message)??null;return e&&o.isPending?a.jsx("div",{className:c.wrap,children:a.jsxs("div",{className:c.emptyState,children:[a.jsx(j,{size:"sm"})," Loading..."]})}):m?a.jsx("div",{className:c.wrap,children:a.jsxs("div",{className:c.emptyState,children:["Error: ",m]})}):a.jsxs("div",{className:c.wrap,"data-molecule-active":r?"true":"false",children:[b&&a.jsx(E,{label:"File changed on disk",variant:"muted",className:c.staleBadge}),a.jsx(S,{ref:u,className:c.molViewer,theme:B,source:_,sourceMode:l.current?"update":"replace",onLoad:()=>{l.current=!0,v(null)},onLoadError:i=>v(i.error),onModifiedChange:i=>{h(i),s==null||s(i)}})]})}const q="_panel_15zlw_1",z={panel:q};function X({tab:e}){const{path:r}=e.data,o=x(t=>t.activeTabId)===e.id;return a.jsx("div",{className:z.panel,children:a.jsx(I,{path:r,isActive:o})})}export{X as MoleculeViewerPanel};
2
- //# sourceMappingURL=MoleculeViewerPanel-BTsiMK_L.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"MoleculeViewerPanel-BTsiMK_L.js","sources":["../../../src/hooks/use-file-watch.ts","../../../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 * `use-file-watch` — subscribes to live daemon `file_changed` pushes for a path (task-006's\n * `FileWatchService`), structurally mirroring `use-checkout-status.ts`: subscribe on mount/path\n * change via `file_watch_subscribe`, route matching pushes through `onSessionMessage`, and on\n * cleanup detach the message handler AND send `file_watch_unsubscribe` for the path being left —\n * `use-checkout-status.ts`'s own header comment calls out that leaving either half undone is the\n * leak this pattern exists to avoid.\n *\n * The actual subscribe/route/cleanup wiring lives in `watchFile`, a plain function with no React\n * dependency (mirrors `molecule-source.ts`/`molecule-reload.ts`'s \"extract the testable core\"\n * convention) — this repo has no DOM test environment set up, so it is the seam\n * `use-file-watch.test.ts` exercises directly with a fake client rather than mounting the hook.\n *\n * A `null` path (an empty \"+\"-menu molecule tab, or the absence of a client) subscribes nothing.\n */\n\nimport { useEffect, useState } from \"react\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\n\ninterface FileChangedMessage {\n type: \"file_changed\";\n path: string;\n}\n\nfunction isFileChangedMessage(msg: unknown): msg is FileChangedMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"file_changed\" &&\n \"path\" in msg &&\n typeof msg.path === \"string\"\n );\n}\n\n/** Subscribes `path` on `client.connection`, calls `onChanged` for every matching `file_changed`\n * push, and returns a cleanup function that detaches the handler AND sends\n * `file_watch_unsubscribe` for `path`. Framework-agnostic on purpose — see module header. */\nexport function watchFile(\n client: Pick<PiStudioClient, \"connection\">,\n path: string,\n onChanged: (changedAt: number) => void,\n): () => void {\n let cancelled = false;\n void client.connection.request(\"file_watch_subscribe\", { path }).catch(() => {});\n\n const unsubscribeMessages = client.connection.onSessionMessage((msg) => {\n if (cancelled) return;\n const raw: unknown = msg;\n if (!isFileChangedMessage(raw)) return;\n if (raw.path !== path) return;\n onChanged(Date.now());\n });\n\n return () => {\n if (cancelled) return; // Idempotent: a second call must never double-send the RPC.\n cancelled = true;\n unsubscribeMessages();\n void client.connection.request(\"file_watch_unsubscribe\", { path }).catch(() => {});\n };\n}\n\nexport interface UseFileWatchResult {\n /** Monotonic timestamp of the last `file_changed` push for this path; `null` if none yet. */\n changedAt: number | null;\n}\n\nexport function useFileWatch(path: string | null): UseFileWatchResult {\n const client = useConnectionStore((s) => s.client);\n const [changedAt, setChangedAt] = useState<number | null>(null);\n\n useEffect(() => {\n if (!client || !path) return;\n return watchFile(client, path, setChangedAt);\n }, [client, path]);\n\n return { changedAt };\n}\n","/**\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":["isFileChangedMessage","msg","watchFile","client","path","onChanged","cancelled","unsubscribeMessages","raw","useFileWatch","useConnectionStore","s","changedAt","setChangedAt","useState","useEffect","moleculeSource","objectUrl","shouldApplyRefresh","lastAppliedAt","modified","MOLVIEWER_THEME","MoleculeViewer","isActive","onModifiedChange","download","useFileDownload","handleRef","useRef","hasLoadedRef","loadError","setLoadError","setModified","lastAppliedAtRef","source","_a","hasUnappliedChange","errorMessage","jsx","styles","jsxs","Spinner","StatusBadge","MolViewer","e","m","MoleculeViewerPanel","tab","useTabStore"],"mappings":"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"}
@@ -1,2 +0,0 @@
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-CeQxaU3V.js";import{u as b,F as k,C as y}from"./CodeView-BO9eNSWw.js";import{u as z}from"./vendor-query-chxBrUuj.js";import{u as E}from"./use-file-download-BDc3Flim.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(o){const{enabled:s,download:e,decode:n}=o;return{isLoading:s&&(e.isLoading||e.hasObjectUrl&&n.isLoading),isError:e.isError||n.isError,error:e.isError?e.error:n.error,data:n.data}}function v(o,s=!0){var a;const e=E(o,s),n=(a=e.data)==null?void 0:a.objectUrl,t=z({queryKey:_.fileText(o),queryFn:async()=>({content:await(await fetch(n)).text()}),enabled:s&&!!n});return h({enabled:s,download:{isLoading:e.isLoading,isError:e.isError,error:e.error,hasObjectUrl:!!n},decode:{isLoading:t.isLoading,isError:t.isError,error:t.error,data:t.data}})}function U(o){const{maxDisplayBytes:s,inline:e,streamed:n,download:t}=o;if(e.isLoading)return{kind:"loading"};if(e.tooLarge){const{size:a}=e.tooLarge;return a>s?{kind:"too-large",size:a,maxDisplayBytes:s,downloading:t.requested&&t.isLoading,downloadUrl:t.objectUrl,downloadName:t.fileName}:n.isLoading?{kind:"streaming",size:a}:n.isError?{kind:"stream-error",size:a,message:n.errorMessage??"stream failed"}:n.content!==null?{kind:"streamed",size:a,content:n.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(o){return`${(o/(1024*1024)).toFixed(1)} MB`}function X({path:o}){var f,x,p,w;const s=b(o),e=s.error instanceof k?s.error:null,n=e!==null&&e.size<=L,t=v(o,n),[a,g]=N.useState(!1),m=E(o,a),i=U({maxDisplayBytes:L,inline:{isLoading:s.isLoading,isError:s.isError,tooLarge:e?{size:e.size}:null,errorMessage:s.error instanceof Error?s.error.message:null,content:((f=s.data)==null?void 0:f.content)??null},streamed:{isLoading:t.isLoading,isError:t.isError,errorMessage:t.error instanceof Error?t.error.message:null,content:((x=t.data)==null?void 0:x.content)??null},download:{requested:a,isLoading: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:o,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:o,content:i.content})})]});case"too-large":{const S=o.split("/").pop()||o;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-C76HR2kP.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TextViewer-C76HR2kP.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),\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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"}