@av-pi-studio/web-client 0.0.41 → 0.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/web/assets/{BinaryFallbackViewer-Cb-P_8V-.js → BinaryFallbackViewer-DLQ4v_lz.js} +2 -2
- package/dist/web/assets/{BinaryFallbackViewer-Cb-P_8V-.js.map → BinaryFallbackViewer-DLQ4v_lz.js.map} +1 -1
- package/dist/web/assets/ChatPanel-C1HzdCye.js +3 -0
- package/dist/web/assets/ChatPanel-C1HzdCye.js.map +1 -0
- package/dist/web/assets/{ChatPanel-CTnpkb2U.css → ChatPanel-CApIhbgZ.css} +1 -1
- package/dist/web/assets/{CodeView-B1cJEve4.js → CodeView-xQuU61a5.js} +2 -2
- package/dist/web/assets/{CodeView-B1cJEve4.js.map → CodeView-xQuU61a5.js.map} +1 -1
- package/dist/web/assets/{FilePanel-YoyZJ2v2.js → FilePanel-B4piK5Uy.js} +3 -3
- package/dist/web/assets/{FilePanel-YoyZJ2v2.js.map → FilePanel-B4piK5Uy.js.map} +1 -1
- package/dist/web/assets/{ImageViewer-BvKE7WrT.js → ImageViewer-Bopoc7w2.js} +2 -2
- package/dist/web/assets/{ImageViewer-BvKE7WrT.js.map → ImageViewer-Bopoc7w2.js.map} +1 -1
- package/dist/web/assets/{MarkdownFileViewer-Ct7fWGmR.js → MarkdownFileViewer-DaZz7dJj.js} +2 -2
- package/dist/web/assets/{MarkdownFileViewer-Ct7fWGmR.js.map → MarkdownFileViewer-DaZz7dJj.js.map} +1 -1
- package/dist/web/assets/{TerminalPanel-rKZq5rWo.js → TerminalPanel-CcwoNqYM.js} +2 -2
- package/dist/web/assets/{TerminalPanel-rKZq5rWo.js.map → TerminalPanel-CcwoNqYM.js.map} +1 -1
- package/dist/web/assets/{TextViewer-BsdUzTN-.js → TextViewer-QxKBHSLd.js} +2 -2
- package/dist/web/assets/{TextViewer-BsdUzTN-.js.map → TextViewer-QxKBHSLd.js.map} +1 -1
- package/dist/web/assets/{VideoViewer-BoA5BwxX.js → VideoViewer-Bno2nZH6.js} +2 -2
- package/dist/web/assets/{VideoViewer-BoA5BwxX.js.map → VideoViewer-Bno2nZH6.js.map} +1 -1
- package/dist/web/assets/index-BaRRHXi4.js +3 -0
- package/dist/web/assets/index-BaRRHXi4.js.map +1 -0
- package/dist/web/assets/{index-EefWgf31.css → index-BsG4HHRz.css} +1 -1
- package/dist/web/assets/{use-file-download-CwyNTuhL.js → use-file-download-Bte2Es0y.js} +2 -2
- package/dist/web/assets/{use-file-download-CwyNTuhL.js.map → use-file-download-Bte2Es0y.js.map} +1 -1
- package/dist/web/assets/{vendor-icons-CX25WOn8.js → vendor-icons-CRg_DLtV.js} +46 -41
- package/dist/web/assets/vendor-icons-CRg_DLtV.js.map +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +3 -3
- package/dist/web/assets/ChatPanel-D_3cLBoK.js +0 -3
- package/dist/web/assets/ChatPanel-D_3cLBoK.js.map +0 -1
- package/dist/web/assets/index-BTP_UxAo.js +0 -3
- package/dist/web/assets/index-BTP_UxAo.js.map +0 -1
- package/dist/web/assets/vendor-icons-CX25WOn8.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
._badge_jtokc_3{display:inline-flex;align-items:center;justify-content:center;padding:1px 6px;border-radius:9999px;border:1px solid;font-size:var(--pi-font-size-xs, 12px);font-family:var(--pi-font-ui, system-ui, sans-serif);font-weight:500;white-space:nowrap;line-height:1.5}._row_5yddc_3{padding:8px 12px;border-radius:var(--pi-borderRadius-md, 6px);max-width:85%;white-space:pre-wrap;word-break:break-word;line-height:1.5;font-size:13px}._who_5yddc_13{display:block;font-size:10px;color:var(--pi-color-foregroundMuted, #7a8394);margin-bottom:3px;text-transform:uppercase;letter-spacing:.03em}._queuedBadge_5yddc_22{display:inline-flex;align-items:center;gap:2px;margin-left:6px;padding:0 4px;border-radius:3px;font-size:9px;font-weight:600;text-transform:none;letter-spacing:normal;vertical-align:middle;background:#14141873;color:#c7cbd1}._user_5yddc_40{background:var(--pi-color-accentMuted, #1e3a5f);color:var(--pi-color-foreground, #dbe9ff);align-self:flex-end;border-bottom-right-radius:2px}._userPending_5yddc_47{opacity:.6}._userQueued_5yddc_51{opacity:.88}._userFailed_5yddc_55{background:var(--pi-color-destructive, #3a1515);color:var(--pi-color-destructiveForeground, #ff8a80);opacity:1}._userImages_5yddc_61{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}._userImage_5yddc_61{max-width:200px;max-height:150px;border-radius:4px;display:block;object-fit:cover;cursor:pointer}._imageDialog_5yddc_77{max-width:90vw}._imageDialogPreview_5yddc_81{display:block;max-width:90vw;max-height:80vh;object-fit:contain}._assistant_5yddc_90{background:var(--pi-color-surface, #1c1f26);color:var(--pi-color-foreground, #d6dae0);align-self:flex-start;border-bottom-left-radius:2px}._assistant_5yddc_90._streaming_5yddc_97{border-left:2px solid var(--pi-color-accent, #2e5cb8)}._streamingText_5yddc_101{white-space:pre-wrap;word-break:break-word}._cursor_5yddc_106{display:inline-block;margin-left:1px;animation:_blink_5yddc_1 1s step-start infinite;color:var(--pi-color-accentBright, #a2b4d7)}@keyframes _blink_5yddc_1{50%{opacity:0}}._reasoning_5yddc_121{background:var(--pi-color-background, #0f1115);color:var(--pi-color-foregroundMuted, #7a8394);font-style:italic;font-size:12px;align-self:flex-start}._tool_5yddc_131{background:var(--pi-color-surface, #1c1f26);border:1px solid var(--pi-color-border, #2c313b);align-self:flex-start;font-size:12px;max-width:90%;padding:0;overflow:hidden}._toolHeader_5yddc_141{display:flex;align-items:center;gap:6px;width:100%;padding:8px 12px;background:none;border:none;color:inherit;font:inherit;text-align:left;cursor:pointer}._toolHeader_5yddc_141:disabled{cursor:default}._toolChevron_5yddc_159{flex-shrink:0;color:var(--pi-color-foregroundMuted, #7a8394);transition:transform .12s ease}._toolChevronOpen_5yddc_165{transform:rotate(90deg)}._toolInlineDetail_5yddc_169{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--pi-color-foregroundMuted, #9aa0a6);font-family:var(--pi-font-mono, ui-monospace, monospace);min-width:0;flex:1}._toolBody_5yddc_179{border-top:1px solid var(--pi-color-border, #2c313b);max-height:420px;overflow:auto}._toolCode_5yddc_185{margin:0;padding:8px 12px;font-family:var(--pi-font-mono, ui-monospace, monospace);font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--pi-color-foreground, #dbe3f0)}._toolIcon_5yddc_196{display:flex;align-items:center;flex-shrink:0}._toolIcon_5yddc_196._running_5yddc_202{color:var(--pi-color-accentBright, #a2b4d7);animation:_pulse_5yddc_1 1.2s ease-in-out infinite}._toolIcon_5yddc_196._completed_5yddc_207{color:var(--pi-color-statusSuccess, #27ae60)}._toolIcon_5yddc_196._error_5yddc_211{color:var(--pi-color-statusDanger, #c0392b)}@keyframes _pulse_5yddc_1{50%{opacity:.5}}._toolLabel_5yddc_221{font-weight:600;text-transform:capitalize}._toolBadge_5yddc_226{margin-left:auto}._error_5yddc_211{background:var(--pi-color-destructive, #3a1515);color:var(--pi-color-destructiveForeground, #ff8a80);align-self:flex-start}._system_5yddc_239{background:transparent;color:var(--pi-color-foregroundMuted, #7a8394);font-style:italic;align-self:flex-start;padding:4px 12px}._viewport_1byko_3{flex:1 1 auto;overflow-y:auto;min-height:0;padding:16px 20px}._inner_1byko_10{position:relative;width:100%}._rowWrap_1byko_15{position:absolute;top:0;left:0;width:100%;display:flex;flex-direction:column;padding-bottom:4px}._empty_1byko_25{display:flex;align-items:center;justify-content:center;height:100%;color:var(--pi-color-foregroundMuted, #7a8394);font-size:13px}._working_1byko_34{display:flex;align-items:center;gap:8px;padding:8px 2px 4px;color:var(--pi-color-foregroundMuted, #7a8394);font-size:13px}._workingDots_1byko_43{display:inline-flex;gap:4px}._workingDots_1byko_43 span{width:6px;height:6px;border-radius:50%;background:var(--pi-color-accentBright, #a2b4d7);animation:_workingBounce_1byko_1 1.2s ease-in-out infinite}._workingDots_1byko_43 span:nth-child(2){animation-delay:.16s}._workingDots_1byko_43 span:nth-child(3){animation-delay:.32s}@keyframes _workingBounce_1byko_1{0%,80%,to{opacity:.3;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}}._strip_5sm81_4{display:flex;gap:6px;flex-wrap:wrap;padding:6px 0 0}._thumb_5sm81_11{position:relative;width:48px;height:48px;border-radius:4px;overflow:hidden;border:1px solid var(--pi-color-border, #2c313b);flex-shrink:0}._thumb_5sm81_11 img{width:100%;height:100%;object-fit:cover;display:block}._remove_5sm81_28{position:absolute;top:-2px;right:-2px;width:16px;height:16px;background:var(--pi-color-statusDanger, #c0392b);color:#fff;border:none;border-radius:50%;font-size:10px;line-height:16px;text-align:center;cursor:pointer;padding:0}.
|
|
1
|
+
._badge_jtokc_3{display:inline-flex;align-items:center;justify-content:center;padding:1px 6px;border-radius:9999px;border:1px solid;font-size:var(--pi-font-size-xs, 12px);font-family:var(--pi-font-ui, system-ui, sans-serif);font-weight:500;white-space:nowrap;line-height:1.5}._row_5yddc_3{padding:8px 12px;border-radius:var(--pi-borderRadius-md, 6px);max-width:85%;white-space:pre-wrap;word-break:break-word;line-height:1.5;font-size:13px}._who_5yddc_13{display:block;font-size:10px;color:var(--pi-color-foregroundMuted, #7a8394);margin-bottom:3px;text-transform:uppercase;letter-spacing:.03em}._queuedBadge_5yddc_22{display:inline-flex;align-items:center;gap:2px;margin-left:6px;padding:0 4px;border-radius:3px;font-size:9px;font-weight:600;text-transform:none;letter-spacing:normal;vertical-align:middle;background:#14141873;color:#c7cbd1}._user_5yddc_40{background:var(--pi-color-accentMuted, #1e3a5f);color:var(--pi-color-foreground, #dbe9ff);align-self:flex-end;border-bottom-right-radius:2px}._userPending_5yddc_47{opacity:.6}._userQueued_5yddc_51{opacity:.88}._userFailed_5yddc_55{background:var(--pi-color-destructive, #3a1515);color:var(--pi-color-destructiveForeground, #ff8a80);opacity:1}._userImages_5yddc_61{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}._userImage_5yddc_61{max-width:200px;max-height:150px;border-radius:4px;display:block;object-fit:cover;cursor:pointer}._imageDialog_5yddc_77{max-width:90vw}._imageDialogPreview_5yddc_81{display:block;max-width:90vw;max-height:80vh;object-fit:contain}._assistant_5yddc_90{background:var(--pi-color-surface, #1c1f26);color:var(--pi-color-foreground, #d6dae0);align-self:flex-start;border-bottom-left-radius:2px}._assistant_5yddc_90._streaming_5yddc_97{border-left:2px solid var(--pi-color-accent, #2e5cb8)}._streamingText_5yddc_101{white-space:pre-wrap;word-break:break-word}._cursor_5yddc_106{display:inline-block;margin-left:1px;animation:_blink_5yddc_1 1s step-start infinite;color:var(--pi-color-accentBright, #a2b4d7)}@keyframes _blink_5yddc_1{50%{opacity:0}}._reasoning_5yddc_121{background:var(--pi-color-background, #0f1115);color:var(--pi-color-foregroundMuted, #7a8394);font-style:italic;font-size:12px;align-self:flex-start}._tool_5yddc_131{background:var(--pi-color-surface, #1c1f26);border:1px solid var(--pi-color-border, #2c313b);align-self:flex-start;font-size:12px;max-width:90%;padding:0;overflow:hidden}._toolHeader_5yddc_141{display:flex;align-items:center;gap:6px;width:100%;padding:8px 12px;background:none;border:none;color:inherit;font:inherit;text-align:left;cursor:pointer}._toolHeader_5yddc_141:disabled{cursor:default}._toolChevron_5yddc_159{flex-shrink:0;color:var(--pi-color-foregroundMuted, #7a8394);transition:transform .12s ease}._toolChevronOpen_5yddc_165{transform:rotate(90deg)}._toolInlineDetail_5yddc_169{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--pi-color-foregroundMuted, #9aa0a6);font-family:var(--pi-font-mono, ui-monospace, monospace);min-width:0;flex:1}._toolBody_5yddc_179{border-top:1px solid var(--pi-color-border, #2c313b);max-height:420px;overflow:auto}._toolCode_5yddc_185{margin:0;padding:8px 12px;font-family:var(--pi-font-mono, ui-monospace, monospace);font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--pi-color-foreground, #dbe3f0)}._toolIcon_5yddc_196{display:flex;align-items:center;flex-shrink:0}._toolIcon_5yddc_196._running_5yddc_202{color:var(--pi-color-accentBright, #a2b4d7);animation:_pulse_5yddc_1 1.2s ease-in-out infinite}._toolIcon_5yddc_196._completed_5yddc_207{color:var(--pi-color-statusSuccess, #27ae60)}._toolIcon_5yddc_196._error_5yddc_211{color:var(--pi-color-statusDanger, #c0392b)}@keyframes _pulse_5yddc_1{50%{opacity:.5}}._toolLabel_5yddc_221{font-weight:600;text-transform:capitalize}._toolBadge_5yddc_226{margin-left:auto}._error_5yddc_211{background:var(--pi-color-destructive, #3a1515);color:var(--pi-color-destructiveForeground, #ff8a80);align-self:flex-start}._system_5yddc_239{background:transparent;color:var(--pi-color-foregroundMuted, #7a8394);font-style:italic;align-self:flex-start;padding:4px 12px}._viewport_1byko_3{flex:1 1 auto;overflow-y:auto;min-height:0;padding:16px 20px}._inner_1byko_10{position:relative;width:100%}._rowWrap_1byko_15{position:absolute;top:0;left:0;width:100%;display:flex;flex-direction:column;padding-bottom:4px}._empty_1byko_25{display:flex;align-items:center;justify-content:center;height:100%;color:var(--pi-color-foregroundMuted, #7a8394);font-size:13px}._working_1byko_34{display:flex;align-items:center;gap:8px;padding:8px 2px 4px;color:var(--pi-color-foregroundMuted, #7a8394);font-size:13px}._workingDots_1byko_43{display:inline-flex;gap:4px}._workingDots_1byko_43 span{width:6px;height:6px;border-radius:50%;background:var(--pi-color-accentBright, #a2b4d7);animation:_workingBounce_1byko_1 1.2s ease-in-out infinite}._workingDots_1byko_43 span:nth-child(2){animation-delay:.16s}._workingDots_1byko_43 span:nth-child(3){animation-delay:.32s}@keyframes _workingBounce_1byko_1{0%,80%,to{opacity:.3;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}}._strip_5sm81_4{display:flex;gap:6px;flex-wrap:wrap;padding:6px 0 0}._thumb_5sm81_11{position:relative;width:48px;height:48px;border-radius:4px;overflow:hidden;border:1px solid var(--pi-color-border, #2c313b);flex-shrink:0}._thumb_5sm81_11 img{width:100%;height:100%;object-fit:cover;display:block}._remove_5sm81_28{position:absolute;top:-2px;right:-2px;width:16px;height:16px;background:var(--pi-color-statusDanger, #c0392b);color:#fff;border:none;border-radius:50%;font-size:10px;line-height:16px;text-align:center;cursor:pointer;padding:0}._composer_1mtoc_1{padding:10px 16px;border-top:1px solid var(--pi-color-border, #2c313b);background:var(--pi-color-surface, #14161a);display:flex;gap:8px;align-items:center;flex-shrink:0}._inputArea_1mtoc_13{flex:1;display:flex;flex-direction:column;gap:0;min-width:0}._textarea_1mtoc_21{resize:none;min-height:38px;max-height:160px;overflow-y:auto}._attachBtn_1mtoc_28{flex-shrink:0}._slashBtn_1mtoc_32{flex-shrink:0;border-color:var(--pi-color-border, #3f3f46)}._textareaWrap_1mtoc_42{position:relative;display:flex;min-width:0}._textareaWrap_1mtoc_42 ._textarea_1mtoc_21{position:relative}._highlightLayer_1mtoc_50{position:absolute;top:0;right:0;bottom:0;left:0;padding:8px 12px;border:1px solid transparent;box-sizing:border-box;font-size:var(--pi-font-size-base, 16px);font-family:var(--pi-font-ui, system-ui, sans-serif);line-height:normal;white-space:pre-wrap;overflow-wrap:break-word;overflow:hidden;color:transparent;pointer-events:none}._commandMark_1mtoc_65{background:color-mix(in srgb,var(--pi-color-accent, #2e5cb8) 24%,transparent);color:transparent;border-radius:4px;box-shadow:0 0 0 1px color-mix(in srgb,var(--pi-color-accent, #2e5cb8) 55%,transparent),0 0 0 3px color-mix(in srgb,var(--pi-color-accent, #2e5cb8) 24%,transparent)}._textareaOverlaid_1mtoc_90{background:transparent!important}._hiddenFileInput_1mtoc_94{display:none}._actions_1mtoc_98{display:flex;flex-direction:row;gap:4px;flex-shrink:0}._panel_12s9l_1{display:flex;flex-direction:column;height:100%;min-height:0}._empty_12s9l_8{display:flex;align-items:center;justify-content:center;height:100%;color:var(--pi-color-foregroundMuted, #7a8394);font-size:13px}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{u as l}from"./vendor-query-AtKBh_Yt.js";import{a as f,r as p}from"./index-
|
|
2
|
-
//# sourceMappingURL=CodeView-
|
|
1
|
+
import{u as l}from"./vendor-query-AtKBh_Yt.js";import{a as f,r as p}from"./index-BaRRHXi4.js";import{j as i,r as u}from"./vendor-react-BoIQBqDW.js";import{O as d,P as m,Q as g,S as h,T as w,U as x}from"./vendor--os7hw7d.js";function q(r,n=!0){const t=f(e=>e.client);return l({queryKey:p.fileRead(r),queryFn:async()=>{if(!t)throw new Error("not connected");const e=await t.connection.request("file_read_request",{path:r});if(!e.ok)throw new Error(e.error??"file read failed");return{content:e.content??"",size:e.size??0}},enabled:!!t&&!!r&&n})}const y="_wrap_17s3u_1",E={wrap:y},c=new Map;function C(r){const[n,t]=u.useState(null);return u.useEffect(()=>{const e=w.matchFilename(x,r);if(!e){t(null);return}const s=c.get(e.name);if(s){t(s);return}let a=!1;return t(null),e.load().then(o=>{c.set(e.name,o),a||t(o)}),()=>{a=!0}},[r]),n?[n]:[]}function v({path:r,content:n}){const t=C(r),e=[d.lineWrapping,m(),...t];return i.jsx("div",{className:E.wrap,children:i.jsx(g,{value:n,theme:h,extensions:e,editable:!1,readOnly:!0,basicSetup:{highlightActiveLine:!1,foldGutter:!1,searchKeymap:!1},height:"100%"})})}export{v as C,q as u};
|
|
2
|
+
//# sourceMappingURL=CodeView-xQuU61a5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeView-
|
|
1
|
+
{"version":3,"file":"CodeView-xQuU61a5.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\ninterface FileReadRpcResponse {\n ok: boolean;\n content?: string;\n size?: number;\n error?: string;\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 if (!response.ok) throw new Error(response.error ?? \"file read failed\");\n return { content: response.content ?? \"\", size: response.size ?? 0 };\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":["useFileRead","path","enabled","client","useConnectionStore","s","useQuery","rpcKeys","response","supportCache","useLanguageExtension","support","setSupport","useState","useEffect","desc","LanguageDescription","languages","cached","cancelled","loaded","CodeView","content","languageExt","extensions","EditorView","foldGutter","jsx","styles","CodeMirror","githubDark"],"mappings":"gOAqBO,SAASA,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,MAAMK,EAAW,MAAML,EAAO,WAAW,QAA6B,oBAAqB,CACzF,KAAAF,CAAA,CACD,EACD,GAAI,CAACO,EAAS,GAAI,MAAM,IAAI,MAAMA,EAAS,OAAS,kBAAkB,EACtE,MAAO,CAAE,QAASA,EAAS,SAAW,GAAI,KAAMA,EAAS,MAAQ,CAAA,CACnE,EACA,QAAS,EAAQL,GAAW,EAAQF,GAASC,CAAA,CAC9C,CACH,oCCVMO,MAAmB,IAEzB,SAASC,EAAqBT,EAA2B,CACvD,KAAM,CAACU,EAASC,CAAU,EAAIC,EAAAA,SAAiC,IAAI,EAEnEC,OAAAA,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAOC,EAAoB,cAAcC,EAAWhB,CAAI,EAC9D,GAAI,CAACc,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,CAAClB,CAAI,CAAC,EAEFU,EAAU,CAACA,CAAO,EAAI,CAAA,CAC/B,CAEO,SAASU,EAAS,CAAE,KAAApB,EAAM,QAAAqB,GAA0B,CACzD,MAAMC,EAAcb,EAAqBT,CAAI,EACvCuB,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,3 +1,3 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/TextViewer-
|
|
2
|
-
import{r as d,j as i}from"./vendor-react-BoIQBqDW.js";import{C as h}from"./vendor--os7hw7d.js";import{a as b,r as v,B as _,S as p}from"./index-
|
|
3
|
-
//# sourceMappingURL=FilePanel-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/TextViewer-QxKBHSLd.js","assets/vendor-react-BoIQBqDW.js","assets/index-BaRRHXi4.js","assets/vendor-query-AtKBh_Yt.js","assets/vendor--os7hw7d.js","assets/vendor-markdown-B8HSIhaA.js","assets/vendor-highlight-DCzuSv8g.js","assets/vendor-icons-CRg_DLtV.js","assets/vendor-overlays-UwLNGk04.js","assets/vendor-dnd-D9kA7FgH.js","assets/index-BsG4HHRz.css","assets/CodeView-xQuU61a5.js","assets/CodeView-CLRODhB8.css","assets/DiffView-DlVCPvLc.js","assets/DiffView-CRbeRztO.css","assets/MarkdownFileViewer-DaZz7dJj.js","assets/markdown-D9FuaV1m.js","assets/markdown-BQwXm0MF.css","assets/MarkdownFileViewer-BD5Ri_Fb.css","assets/ImageViewer-Bopoc7w2.js","assets/use-file-download-Bte2Es0y.js","assets/ImageViewer-ijzMsq8H.css","assets/VideoViewer-Bno2nZH6.js","assets/VideoViewer-BBuZ4wou.css","assets/BinaryFallbackViewer-DLQ4v_lz.js","assets/BinaryFallbackViewer-CLiNWU_S.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{r as d,j as i}from"./vendor-react-BoIQBqDW.js";import{C as h}from"./vendor--os7hw7d.js";import{a as b,r as v,B as _,S as p}from"./index-BaRRHXi4.js";import{u as x}from"./vendor-query-AtKBh_Yt.js";import{D as E}from"./DiffView-DlVCPvLc.js";import{_ as f}from"./vendor-highlight-DCzuSv8g.js";function V(e,a,t,r=!0){const o=b(n=>n.client);return x({queryKey:v.fileDiff(e,a,t),queryFn:async()=>{if(!o)throw new Error("not connected");return{patch:(await o.connection.request("file_diff_request",{path:e,cwd:a,staged:t})).patch??""}},enabled:!!o&&!!e&&!!a&&r})}const j=d.lazy(()=>f(()=>import("./TextViewer-QxKBHSLd.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14])).then(e=>({default:e.TextViewer}))),T=d.lazy(()=>f(()=>import("./MarkdownFileViewer-DaZz7dJj.js"),__vite__mapDeps([15,1,2,3,4,5,6,7,8,9,10,11,12,16,17,13,14,18])).then(e=>({default:e.MarkdownFileViewer}))),k=d.lazy(()=>f(()=>import("./ImageViewer-Bopoc7w2.js"),__vite__mapDeps([19,1,2,3,4,5,6,7,8,9,10,20,13,14,21])).then(e=>({default:e.ImageViewer}))),z=d.lazy(()=>f(()=>import("./VideoViewer-Bno2nZH6.js"),__vite__mapDeps([22,1,2,3,4,5,6,7,8,9,10,20,13,14,23])).then(e=>({default:e.VideoViewer}))),D=d.lazy(()=>f(()=>import("./BinaryFallbackViewer-DLQ4v_lz.js"),__vite__mapDeps([24,1,2,3,4,5,6,7,8,9,10,20,25])).then(e=>({default:e.BinaryFallbackViewer}))),I={text:j,markdown:T,image:k,video:z,binary:D},S={md:"markdown",markdown:"markdown",mdx:"markdown",png:"image",jpg:"image",jpeg:"image",gif:"image",webp:"image",svg:"image",bmp:"image",ico:"image",avif:"image",mp4:"video",webm:"video",mov:"video",m4v:"video",ogv:"video",pdf:"binary",zip:"binary",gz:"binary",tar:"binary",exe:"binary",wasm:"binary",woff:"binary",woff2:"binary",ttf:"binary",otf:"binary"},N=[["image/","image"],["video/","video"]];function F(e){const a=e.split("/").pop()??e,t=a.lastIndexOf(".");return t>0?a.slice(t+1).toLowerCase():""}function O(e,a={}){const t=S[F(e)];if(t)return t;const r=a.mimeHint??"";for(const[o,n]of N)if(r.startsWith(o))return n;return a.isBinary?"binary":"text"}const P="_panel_i61bw_1",R="_header_i61bw_7",B="_path_i61bw_17",L="_size_i61bw_24",M="_viewToggle_i61bw_29",A="_body_i61bw_34",C="_emptyState_i61bw_39",K="_diffEmpty_i61bw_48",s={panel:P,header:R,path:B,size:L,viewToggle:M,body:A,emptyState:C,diffEmpty:K};function u(e){return"path"in e}function q(e){return"staged"in e}function W({tab:e}){var m;const a=e.kind==="diff",t=e.data,r=u(t)?t.path:"",o=u(t)&&q(t)?t.staged:!1,[n,l]=d.useState(a?"diff":"file"),w=e.workspaceCwd||"~",c=V(r,w,o,n==="diff"),y=O(r),g=I[y];return i.jsxs("div",{className:s.panel,children:[i.jsxs("div",{className:s.header,children:[i.jsx("span",{className:s.path,children:r}),i.jsx("span",{className:s.size,children:n==="diff"?o?"staged":"unstaged":null}),i.jsxs("div",{className:s.viewToggle,children:[i.jsx(_,{size:"xs",variant:n==="file"?"default":"ghost",onClick:()=>l("file"),children:"File"}),i.jsx(_,{size:"xs",variant:n==="diff"?"default":"ghost",onClick:()=>l("diff"),children:"Diff"})]})]}),i.jsx("div",{className:h(s.body,"file-body"),children:n==="file"?i.jsx(d.Suspense,{fallback:i.jsxs("div",{className:s.emptyState,children:[i.jsx(p,{size:"sm"})," Loading..."]}),children:i.jsx(g,{path:r})}):c.isLoading?i.jsxs("div",{className:s.emptyState,children:[i.jsx(p,{size:"sm"})," Loading diff..."]}):c.isError?i.jsxs("div",{className:s.emptyState,children:["Error: ",c.error instanceof Error?c.error.message:"unknown error"]}):(m=c.data)!=null&&m.patch?i.jsx(E,{patch:c.data.patch}):i.jsx("div",{className:s.diffEmpty,children:"No changes (file matches HEAD)"})})]})}const J=Object.freeze(Object.defineProperty({__proto__:null,FilePanel:W},Symbol.toStringTag,{value:"Module"}));export{J as F,s as p};
|
|
3
|
+
//# sourceMappingURL=FilePanel-B4piK5Uy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"mappings":"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","names":["useFileDiff","path","cwd","staged","enabled","client","useConnectionStore","s","useQuery","rpcKeys","TextViewer","lazy","__vitePreload","m","MarkdownFileViewer","ImageViewer","VideoViewer","BinaryFallbackViewer","VIEWER_BY_KIND","EXT_TO_VIEWER","MIME_PREFIX_TO_VIEWER","extOf","name","dot","detectViewerKind","opts","byExt","mime","prefix","kind","hasPath","data","isDiffTabData","FilePanel","tab","isDiffTab","viewMode","setViewMode","useState","diffQuery","viewerKind","Viewer","jsxs","styles","jsx","Button","clsx","Suspense","Spinner","_a","DiffView"],"ignoreList":[],"sources":["../../../src/hooks/use-file-diff.ts","../../../src/features/files/viewer-registry.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 * File-viewer registry — maps a detected file kind to the viewer component that renders it\n * (POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular file preview). `FilePanel` detects the kind from\n * `path` (extension) and the explorer's binary `mimeHint` (for extension-less/ambiguous files),\n * then looks up the viewer here — adding a new file type is one entry in `EXT_TO_VIEWER`/\n * `MIME_PREFIX_TO_VIEWER` plus one entry in `VIEWER_BY_KIND`, no changes to `FilePanel` itself.\n */\n\nimport { lazy, type ComponentType } from \"react\";\n\nexport type ViewerKind = \"text\" | \"markdown\" | \"image\" | \"video\" | \"binary\";\n\nexport interface ViewerProps {\n /** Explorer-relative or absolute path, as opened from the file tree/diff panel. */\n path: string;\n}\n\nconst TextViewer = lazy(() => import(\"./TextViewer.js\").then((m) => ({ default: m.TextViewer })));\nconst MarkdownFileViewer = lazy(() =>\n import(\"./MarkdownFileViewer.js\").then((m) => ({ default: m.MarkdownFileViewer })),\n);\nconst ImageViewer = lazy(() => import(\"./ImageViewer.js\").then((m) => ({ default: m.ImageViewer })));\nconst VideoViewer = lazy(() => import(\"./VideoViewer.js\").then((m) => ({ default: m.VideoViewer })));\nconst BinaryFallbackViewer = lazy(() =>\n import(\"./BinaryFallbackViewer.js\").then((m) => ({ default: m.BinaryFallbackViewer })),\n);\n\n/** One component per `ViewerKind`. Register a new file type here — nowhere else. */\nexport const VIEWER_BY_KIND: Record<ViewerKind, ComponentType<ViewerProps>> = {\n text: TextViewer,\n markdown: MarkdownFileViewer,\n image: ImageViewer,\n video: VideoViewer,\n binary: BinaryFallbackViewer,\n};\n\nconst EXT_TO_VIEWER: Record<string, ViewerKind> = {\n md: \"markdown\",\n markdown: \"markdown\",\n mdx: \"markdown\",\n\n png: \"image\",\n jpg: \"image\",\n jpeg: \"image\",\n gif: \"image\",\n webp: \"image\",\n svg: \"image\",\n bmp: \"image\",\n ico: \"image\",\n avif: \"image\",\n\n mp4: \"video\",\n webm: \"video\",\n mov: \"video\",\n m4v: \"video\",\n ogv: \"video\",\n\n // No dedicated preview yet — show the binary fallback (name + manual download) instead of\n // dumping raw/garbled bytes into the text viewer.\n pdf: \"binary\",\n zip: \"binary\",\n gz: \"binary\",\n tar: \"binary\",\n exe: \"binary\",\n wasm: \"binary\",\n woff: \"binary\",\n woff2: \"binary\",\n ttf: \"binary\",\n otf: \"binary\",\n};\n\nconst MIME_PREFIX_TO_VIEWER: Array<[prefix: string, kind: ViewerKind]> = [\n [\"image/\", \"image\"],\n [\"video/\", \"video\"],\n];\n\nfunction extOf(path: string): string {\n const name = path.split(\"/\").pop() ?? path;\n const dot = name.lastIndexOf(\".\");\n return dot > 0 ? name.slice(dot + 1).toLowerCase() : \"\";\n}\n\n/**\n * Detects the viewer kind for a file, preferring the extension (fast, always available) and\n * falling back to a server-supplied MIME hint (extension-less files, e.g. binary blobs the\n * explorer sniffed) — `isBinary` further distinguishes an unrecognized binary from unrecognized\n * text, so unknown-but-textual files (`.env`, `.gitignore`) still fall through to `TextViewer`.\n */\nexport function detectViewerKind(\n path: string,\n opts: { mimeHint?: string; isBinary?: boolean } = {},\n): ViewerKind {\n const byExt = EXT_TO_VIEWER[extOf(path)];\n if (byExt) return byExt;\n\n const mime = opts.mimeHint ?? \"\";\n for (const [prefix, kind] of MIME_PREFIX_TO_VIEWER) {\n if (mime.startsWith(prefix)) return kind;\n }\n\n if (opts.isBinary) return \"binary\";\n return \"text\";\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"],"file":"assets/FilePanel-YoyZJ2v2.js"}
|
|
1
|
+
{"version":3,"mappings":"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","names":["useFileDiff","path","cwd","staged","enabled","client","useConnectionStore","s","useQuery","rpcKeys","TextViewer","lazy","__vitePreload","m","MarkdownFileViewer","ImageViewer","VideoViewer","BinaryFallbackViewer","VIEWER_BY_KIND","EXT_TO_VIEWER","MIME_PREFIX_TO_VIEWER","extOf","name","dot","detectViewerKind","opts","byExt","mime","prefix","kind","hasPath","data","isDiffTabData","FilePanel","tab","isDiffTab","viewMode","setViewMode","useState","diffQuery","viewerKind","Viewer","jsxs","styles","jsx","Button","clsx","Suspense","Spinner","_a","DiffView"],"ignoreList":[],"sources":["../../../src/hooks/use-file-diff.ts","../../../src/features/files/viewer-registry.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 * File-viewer registry — maps a detected file kind to the viewer component that renders it\n * (POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular file preview). `FilePanel` detects the kind from\n * `path` (extension) and the explorer's binary `mimeHint` (for extension-less/ambiguous files),\n * then looks up the viewer here — adding a new file type is one entry in `EXT_TO_VIEWER`/\n * `MIME_PREFIX_TO_VIEWER` plus one entry in `VIEWER_BY_KIND`, no changes to `FilePanel` itself.\n */\n\nimport { lazy, type ComponentType } from \"react\";\n\nexport type ViewerKind = \"text\" | \"markdown\" | \"image\" | \"video\" | \"binary\";\n\nexport interface ViewerProps {\n /** Explorer-relative or absolute path, as opened from the file tree/diff panel. */\n path: string;\n}\n\nconst TextViewer = lazy(() => import(\"./TextViewer.js\").then((m) => ({ default: m.TextViewer })));\nconst MarkdownFileViewer = lazy(() =>\n import(\"./MarkdownFileViewer.js\").then((m) => ({ default: m.MarkdownFileViewer })),\n);\nconst ImageViewer = lazy(() => import(\"./ImageViewer.js\").then((m) => ({ default: m.ImageViewer })));\nconst VideoViewer = lazy(() => import(\"./VideoViewer.js\").then((m) => ({ default: m.VideoViewer })));\nconst BinaryFallbackViewer = lazy(() =>\n import(\"./BinaryFallbackViewer.js\").then((m) => ({ default: m.BinaryFallbackViewer })),\n);\n\n/** One component per `ViewerKind`. Register a new file type here — nowhere else. */\nexport const VIEWER_BY_KIND: Record<ViewerKind, ComponentType<ViewerProps>> = {\n text: TextViewer,\n markdown: MarkdownFileViewer,\n image: ImageViewer,\n video: VideoViewer,\n binary: BinaryFallbackViewer,\n};\n\nconst EXT_TO_VIEWER: Record<string, ViewerKind> = {\n md: \"markdown\",\n markdown: \"markdown\",\n mdx: \"markdown\",\n\n png: \"image\",\n jpg: \"image\",\n jpeg: \"image\",\n gif: \"image\",\n webp: \"image\",\n svg: \"image\",\n bmp: \"image\",\n ico: \"image\",\n avif: \"image\",\n\n mp4: \"video\",\n webm: \"video\",\n mov: \"video\",\n m4v: \"video\",\n ogv: \"video\",\n\n // No dedicated preview yet — show the binary fallback (name + manual download) instead of\n // dumping raw/garbled bytes into the text viewer.\n pdf: \"binary\",\n zip: \"binary\",\n gz: \"binary\",\n tar: \"binary\",\n exe: \"binary\",\n wasm: \"binary\",\n woff: \"binary\",\n woff2: \"binary\",\n ttf: \"binary\",\n otf: \"binary\",\n};\n\nconst MIME_PREFIX_TO_VIEWER: Array<[prefix: string, kind: ViewerKind]> = [\n [\"image/\", \"image\"],\n [\"video/\", \"video\"],\n];\n\nfunction extOf(path: string): string {\n const name = path.split(\"/\").pop() ?? path;\n const dot = name.lastIndexOf(\".\");\n return dot > 0 ? name.slice(dot + 1).toLowerCase() : \"\";\n}\n\n/**\n * Detects the viewer kind for a file, preferring the extension (fast, always available) and\n * falling back to a server-supplied MIME hint (extension-less files, e.g. binary blobs the\n * explorer sniffed) — `isBinary` further distinguishes an unrecognized binary from unrecognized\n * text, so unknown-but-textual files (`.env`, `.gitignore`) still fall through to `TextViewer`.\n */\nexport function detectViewerKind(\n path: string,\n opts: { mimeHint?: string; isBinary?: boolean } = {},\n): ViewerKind {\n const byExt = EXT_TO_VIEWER[extOf(path)];\n if (byExt) return byExt;\n\n const mime = opts.mimeHint ?? \"\";\n for (const [prefix, kind] of MIME_PREFIX_TO_VIEWER) {\n if (mime.startsWith(prefix)) return kind;\n }\n\n if (opts.isBinary) return \"binary\";\n return \"text\";\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"],"file":"assets/FilePanel-B4piK5Uy.js"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e}from"./vendor-react-BoIQBqDW.js";import{S as t}from"./index-
|
|
2
|
-
//# sourceMappingURL=ImageViewer-
|
|
1
|
+
import{j as e}from"./vendor-react-BoIQBqDW.js";import{S as t}from"./index-BaRRHXi4.js";import{u as a}from"./use-file-download-Bte2Es0y.js";import{p as s}from"./FilePanel-B4piK5Uy.js";import"./vendor-query-AtKBh_Yt.js";import"./vendor--os7hw7d.js";import"./vendor-markdown-B8HSIhaA.js";import"./vendor-highlight-DCzuSv8g.js";import"./vendor-icons-CRg_DLtV.js";import"./vendor-overlays-UwLNGk04.js";import"./vendor-dnd-D9kA7FgH.js";import"./DiffView-DlVCPvLc.js";const m="_wrap_1al28_1",n="_image_1al28_14",o={wrap:m,image:n};function S({path:i}){const r=a(i);return r.isLoading?e.jsxs("div",{className:s.emptyState,children:[e.jsx(t,{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{S as ImageViewer};
|
|
2
|
+
//# sourceMappingURL=ImageViewer-Bopoc7w2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageViewer-
|
|
1
|
+
{"version":3,"file":"ImageViewer-Bopoc7w2.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":"4gBAYO,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,2 +1,2 @@
|
|
|
1
|
-
import{r as d,j as r}from"./vendor-react-BoIQBqDW.js";import{S as c,B as a}from"./index-
|
|
2
|
-
//# sourceMappingURL=MarkdownFileViewer-
|
|
1
|
+
import{r as d,j as r}from"./vendor-react-BoIQBqDW.js";import{S as c,B as a}from"./index-BaRRHXi4.js";import{u as m,C as p}from"./CodeView-xQuU61a5.js";import{M as l}from"./markdown-D9FuaV1m.js";import{p as n}from"./FilePanel-B4piK5Uy.js";import"./vendor-query-AtKBh_Yt.js";import"./vendor--os7hw7d.js";import"./vendor-markdown-B8HSIhaA.js";import"./vendor-highlight-DCzuSv8g.js";import"./vendor-icons-CRg_DLtV.js";import"./vendor-overlays-UwLNGk04.js";import"./vendor-dnd-D9kA7FgH.js";import"./DiffView-DlVCPvLc.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-DaZz7dJj.js.map
|
package/dist/web/assets/{MarkdownFileViewer-Ct7fWGmR.js.map → MarkdownFileViewer-DaZz7dJj.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkdownFileViewer-
|
|
1
|
+
{"version":3,"file":"MarkdownFileViewer-DaZz7dJj.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":"wmBAoBO,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
|
-
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-BoIQBqDW.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-
|
|
2
|
-
//# sourceMappingURL=TerminalPanel-
|
|
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-BoIQBqDW.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-BaRRHXi4.js";import"./vendor--os7hw7d.js";import"./vendor-query-AtKBh_Yt.js";import"./vendor-icons-CRg_DLtV.js";import"./vendor-overlays-UwLNGk04.js";import"./vendor-dnd-D9kA7FgH.js";import"./vendor-highlight-DCzuSv8g.js";import"./vendor-markdown-B8HSIhaA.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-CcwoNqYM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TerminalPanel-rKZq5rWo.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-CcwoNqYM.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":"ukBAuBO,MAAMA,CAAqB,CAIhC,YAA6BC,EAAsB,CAHlCC,EAAA,uBAAkB,KAC3BA,EAAA,cAA8B,MAET,KAAA,OAAAD,CAAuB,CAGpD,OAAc,CACR,KAAK,SACT,KAAK,OAAS,KAAK,OAAO,gBAAiBE,GAAU,KAAK,SAASA,CAAK,CAAC,EAC3E,CAGA,MAAa,QACXC,EAAA,KAAK,SAAL,MAAAA,EAAA,WACA,KAAK,OAAS,IAChB,CAGA,cAAcC,EAAcC,EAAgD,CAC1E,YAAK,YAAY,IAAID,EAAMC,CAAU,EAC9B,IAAM,CACP,KAAK,YAAY,IAAID,CAAI,IAAMC,GAAY,KAAK,YAAY,OAAOD,CAAI,CAC7E,CACF,CAGA,QAAQA,EAAuB,CAC7B,OAAO,KAAK,YAAY,IAAIA,CAAI,CAClC,CAKA,UAAUA,EAAcE,EAAwB,CAC9C,KAAK,OAAO,WAAWC,EAAoB,CAAE,OAAQ,QAAS,KAAAH,EAAM,KAAAE,CAAA,CAAM,CAAC,CAC7E,CAGA,WAAWF,EAAcI,EAAcC,EAAoB,CACzD,KAAK,OAAO,WAAWF,EAAoB,CAAE,OAAQ,SAAU,KAAAH,EAAM,KAAAI,EAAM,KAAAC,CAAA,CAAM,CAAC,CACpF,CAIQ,SAASP,EAA4B,WAC3C,MAAMG,EAAa,KAAK,YAAY,IAAIH,EAAM,IAAI,EAClD,GAAKG,EACL,OAAQH,EAAM,OAAA,CACZ,IAAK,UACHC,EAAAE,EAAW,WAAX,MAAAF,EAAA,KAAAE,EAAsBH,EAAM,MAC5B,OACF,IAAK,YACHQ,EAAAL,EAAW,aAAX,MAAAK,EAAA,KAAAL,EAAwBH,EAAM,MAC9B,OACF,IAAK,WACHS,EAAAN,EAAW,YAAX,MAAAM,EAAA,KAAAN,EAAuBH,EAAM,MAC7B,OACF,QAEE,MAAA,CAEN,CACF,oGClDMU,MAAqB,QAE3B,SAASC,EAAUb,EAA4C,CAC7D,IAAIc,EAASF,EAAe,IAAIZ,CAAM,EACtC,OAAKc,IACHA,EAAS,IAAIf,EAAqBC,CAAM,EACxCc,EAAO,MAAA,EACPF,EAAe,IAAIZ,EAAQc,CAAM,GAE5BA,CACT,CAGA,MAAMC,EAAiB,CACrB,WAAY,UACZ,WAAY,UACZ,OAAQ,UACR,aAAc,UACd,oBAAqB,yBACrB,MAAO,UACP,IAAK,UACL,MAAO,UACP,OAAQ,UACR,KAAM,UACN,QAAS,UACT,KAAM,UACN,MAAO,UACP,YAAa,UACb,UAAW,UACX,YAAa,UACb,aAAc,UACd,WAAY,UACZ,cAAe,UACf,WAAY,UACZ,YAAa,SACf,EAEO,SAASC,GAAc,CAAE,IAAAC,GAA2B,CACzD,MAAMX,EAAOW,EAAI,KACXC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAE3CC,EADcC,EAAaF,GAAMA,EAAE,WAAW,IACnBH,EAAI,GAE/BM,EAAeC,EAAAA,OAA8B,IAAI,EACjDC,EAAcD,EAAAA,OAAwB,IAAI,EAC1CE,EAAcF,EAAAA,OAAwB,IAAI,EAC1CG,EAAUH,EAAAA,OAAsBlB,EAAK,IAAI,EAIzCsB,EAAYJ,EAAAA,OAAON,CAAM,EAC/BU,EAAU,QAAUV,EAEpB,KAAM,CAACd,EAAMyB,CAAO,EAAIC,EAAAA,SAAwBxB,EAAK,IAAI,EACnD,CAACyB,EAAOC,CAAQ,EAAIF,EAAAA,SAAwB,IAAI,EAoBhDG,EAAeT,EAAAA,OAAO,EAAK,EAC3BU,EAAoBV,EAAAA,OAAO,EAAK,EA2ItC,OA1IAW,EAAAA,UAAU,IAAM,CAEd,GADAF,EAAa,QAAU,GACnB,CAACf,GAAUS,EAAQ,UAAY,MAAQO,EAAkB,QAC3D,MAAO,IAAM,CACXD,EAAa,QAAU,EACzB,EAEFC,EAAkB,QAAU,GAE5B,MAAME,EAAM9B,EAAK,KAAO,IAExB,OAAKY,EAAO,WACT,QAAgC,0BAA2B,CAAE,YAAa,GAAI,IAAAkB,CAAA,CAAK,EACnF,KAAMC,GAAQ,CACb,MAAMC,EAAUD,EAAI,SAAS,KAC7B,GAAI,CAACJ,EAAa,QAAS,CAGpBf,EAAO,WAAW,QAAQ,wBAAyB,CAAE,KAAMoB,CAAA,CAAS,EAAE,MAAM,IAAM,CAAC,CAAC,EACzF,MACF,CACAX,EAAQ,QAAUW,EAClBT,EAAQS,CAAO,EACfhB,EAAY,SAAA,EAAW,WAAWL,EAAI,GAAI,CAAE,KAAMqB,EAAS,CAC7D,CAAC,EACA,MAAOC,GAAiB,CAClBN,EAAa,SAClBD,EAASO,aAAe,MAAQA,EAAI,QAAU,OAAOA,CAAG,CAAC,CAC3D,CAAC,EACA,QAAQ,IAAM,CAKbL,EAAkB,QAAU,EAC9B,CAAC,EAEI,IAAM,CACXD,EAAa,QAAU,EACzB,CAEF,EAAG,CAACf,EAAQD,EAAI,EAAE,CAAC,EAkBnBkB,EAAAA,UAAU,IACD,IAAM,CACX,MAAMK,EAAcb,EAAQ,QACxBa,IAAgB,MACpB,WAAW,IAAM,OACXP,EAAa,UACZ9B,EAAAyB,EAAU,UAAV,MAAAzB,EAAmB,WACrB,QAAQ,wBAAyB,CAAE,KAAMqC,CAAA,GACzC,MAAM,IAAM,CAAC,EAClB,EAAG,CAAC,CACN,EAEC,CAAA,CAAE,EAGLL,EAAAA,UAAU,IAAM,CACd,GAAI,CAACjB,GAAUd,IAAS,MAAQ,CAACmB,EAAa,QAAS,OAEvD,MAAMkB,EAAW,IAAIC,WAAS,CAC5B,YAAa,GACb,WAAY,mEACZ,SAAU,GACV,WAAY,IACZ,MAAO3B,EACP,iBAAkB,EAAA,CACnB,EACK4B,EAAW,IAAIC,WACrBH,EAAS,UAAUE,CAAQ,EAC3BF,EAAS,KAAKlB,EAAa,OAAO,EAClCoB,EAAS,IAAA,EAETlB,EAAY,QAAUgB,EACtBf,EAAY,QAAUiB,EAEtB,MAAM7B,EAASD,EAAUK,EAAO,UAAU,EACpC2B,EAAkB/B,EAAO,cAAcV,EAAM,CACjD,SAAW0C,GAAUL,EAAS,MAAMK,CAAK,EACzC,WAAaA,GAAU,CACrBL,EAAS,MAAA,EACTA,EAAS,MAAMK,CAAK,CACtB,EACA,UAAYA,GAAUL,EAAS,MAAMK,CAAK,CAAA,CAC3C,EAEI5B,EAAO,WAAW,QAAQ,6BAA8B,CAAE,KAAAd,EAAM,EAAE,MAAOmC,GAAiB,CAC7FP,EAASO,aAAe,MAAQA,EAAI,QAAU,OAAOA,CAAG,CAAC,CAC3D,CAAC,EAED,MAAMQ,EAAiBN,EAAS,OAAQK,GAAU,CAChDhC,EAAO,UAAUV,EAAM,IAAI,cAAc,OAAO0C,CAAK,CAAC,CACxD,CAAC,EACKE,EAAmBP,EAAS,SAAS,CAAC,CAAE,KAAAhC,EAAM,KAAAD,KAAW,CAC7DM,EAAO,WAAWV,EAAMI,EAAMC,CAAI,CACpC,CAAC,EAEKwC,EAAiB,IAAI,eAAe,IAAM,CAC9CN,EAAS,IAAA,CACX,CAAC,EACD,OAAAM,EAAe,QAAQ1B,EAAa,OAAO,EAEpC,IAAM,CACX0B,EAAe,WAAA,EACfF,EAAe,QAAA,EACfC,EAAiB,QAAA,EACjBH,EAAA,EACK3B,EAAO,WAAW,QAAQ,+BAAgC,CAAE,KAAAd,CAAA,CAAM,EAAE,MAAM,IAAM,CAAC,CAAC,EACvFqC,EAAS,QAAA,EACThB,EAAY,QAAU,KACtBC,EAAY,QAAU,IACxB,CAEF,EAAG,CAACR,EAAQd,CAAI,CAAC,EAGjB+B,EAAAA,UAAU,IAAM,OACTd,KACLlB,EAAAuB,EAAY,UAAZ,MAAAvB,EAAqB,MACvB,EAAG,CAACkB,CAAQ,CAAC,EAETU,EAEAmB,EAAAA,IAAC,OAAI,UAAWC,EAAO,KACrB,SAAAC,EAAAA,KAAC,MAAA,CAAI,UAAWD,EAAO,OAAQ,SAAA,CAAA,mBAAiBpB,CAAA,CAAA,CAAM,CAAA,CACxD,EAIA3B,IAAS,KAET8C,EAAAA,IAAC,OAAI,UAAWC,EAAO,KACrB,SAAAC,EAAAA,KAAC,MAAA,CAAI,UAAWD,EAAO,OACrB,SAAA,CAAAD,EAAAA,IAACG,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,qBAAA,CAAA,CACvB,CAAA,CACF,EAKFH,EAAAA,IAAC,MAAA,CAAI,UAAWC,EAAO,KACrB,SAAAD,EAAAA,IAAC,MAAA,CAAI,IAAK3B,EAAc,UAAW4B,EAAO,QAAA,CAAU,EACtD,CAEJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e}from"./vendor-react-BoIQBqDW.js";import{S as i}from"./index-
|
|
2
|
-
//# sourceMappingURL=TextViewer-
|
|
1
|
+
import{j as e}from"./vendor-react-BoIQBqDW.js";import{S as i}from"./index-BaRRHXi4.js";import{u as s,C as n}from"./CodeView-xQuU61a5.js";import{p as o}from"./FilePanel-B4piK5Uy.js";import"./vendor-query-AtKBh_Yt.js";import"./vendor--os7hw7d.js";import"./vendor-markdown-B8HSIhaA.js";import"./vendor-highlight-DCzuSv8g.js";import"./vendor-icons-CRg_DLtV.js";import"./vendor-overlays-UwLNGk04.js";import"./vendor-dnd-D9kA7FgH.js";import"./DiffView-DlVCPvLc.js";function E({path:t}){const r=s(t);return r.isLoading?e.jsxs("div",{className:o.emptyState,children:[e.jsx(i,{size:"sm"})," Loading..."]}):r.isError?e.jsxs("div",{className:o.emptyState,children:["Error: ",r.error instanceof Error?r.error.message:"unknown error"]}):r.data?e.jsx(n,{path:t,content:r.data.content}):null}export{E as TextViewer};
|
|
2
|
+
//# sourceMappingURL=TextViewer-QxKBHSLd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextViewer-
|
|
1
|
+
{"version":3,"file":"TextViewer-QxKBHSLd.js","sources":["../../../src/features/files/TextViewer.tsx"],"sourcesContent":["/**\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\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { useFileRead } from \"@pi-studio-ui/hooks/use-file-read.js\";\nimport { CodeView } from \"./CodeView.js\";\nimport type { ViewerProps } from \"./viewer-registry.js\";\nimport styles from \"./FilePanel.module.css\";\n\nexport function TextViewer({ path }: ViewerProps) {\n const query = useFileRead(path);\n\n if (query.isLoading) {\n return (\n <div className={styles.emptyState}>\n <Spinner size=\"sm\" /> Loading...\n </div>\n );\n }\n if (query.isError) {\n return (\n <div className={styles.emptyState}>\n Error: {query.error instanceof Error ? query.error.message : \"unknown error\"}\n </div>\n );\n }\n if (!query.data) return null;\n return <CodeView path={path} content={query.data.content} />;\n}\n"],"names":["TextViewer","path","query","useFileRead","jsxs","styles","jsx","Spinner","CodeView"],"mappings":"2cAYO,SAASA,EAAW,CAAE,KAAAC,GAAqB,CAChD,MAAMC,EAAQC,EAAYF,CAAI,EAE9B,OAAIC,EAAM,UAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,WACrB,SAAA,CAAAC,EAAAA,IAACC,EAAA,CAAQ,KAAK,IAAA,CAAK,EAAE,aAAA,EACvB,EAGAL,EAAM,QAENE,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,WAAY,SAAA,CAAA,UACzBH,EAAM,iBAAiB,MAAQA,EAAM,MAAM,QAAU,eAAA,EAC/D,EAGCA,EAAM,WACHM,EAAA,CAAS,KAAAP,EAAY,QAASC,EAAM,KAAK,QAAS,EADlC,IAE1B"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as o}from"./vendor-react-BoIQBqDW.js";import{S as t}from"./index-
|
|
2
|
-
//# sourceMappingURL=VideoViewer-
|
|
1
|
+
import{j as o}from"./vendor-react-BoIQBqDW.js";import{S as t}from"./index-BaRRHXi4.js";import{u as a}from"./use-file-download-Bte2Es0y.js";import{p as e}from"./FilePanel-B4piK5Uy.js";import"./vendor-query-AtKBh_Yt.js";import"./vendor--os7hw7d.js";import"./vendor-markdown-B8HSIhaA.js";import"./vendor-highlight-DCzuSv8g.js";import"./vendor-icons-CRg_DLtV.js";import"./vendor-overlays-UwLNGk04.js";import"./vendor-dnd-D9kA7FgH.js";import"./DiffView-DlVCPvLc.js";const n="_wrap_1c9y4_1",m="_video_1c9y4_10",i={wrap:n,video:m};function S({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{S as VideoViewer};
|
|
2
|
+
//# sourceMappingURL=VideoViewer-Bno2nZH6.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoViewer-
|
|
1
|
+
{"version":3,"file":"VideoViewer-Bno2nZH6.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":"4gBAYO,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"}
|