window.__ModuleLoader__.load({ id: "dsh-touched-git", factory: (require) => { const react = require("react"); const jsx = require("react/jsx-runtime"); const { DiffViewer } = require("@dsh-external/dsh-diff-viewer"); const workspaceWindows = new Map(); const STYLE_ID = "dsh-touched-git-workspaces-style"; const STYLE_TEXT = ` [class*="_headerActions"]:has(.dsh-git-working-dirs){flex:1} [class*="_titleRow"]:has(.dsh-git-working-dirs)>[class*="_headerUtilities"]{margin-left:8px} .dsh-git-working-dirs{position:relative;margin-left:auto;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family)} .dsh-git-working-dirs__trigger{height:32px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));border-radius:18px;padding:5px 12px;background:transparent;color:inherit;font:13px var(--dsw-font-family);white-space:nowrap;cursor:pointer} .dsh-git-working-dirs__trigger:hover,.dsh-git-working-dirs__trigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover,#303846)} .dsh-git-working-dirs__backdrop{position:fixed;inset:0;z-index:99;border:0;background:transparent;padding:0} .dsh-git-working-dirs__menu{position:absolute;z-index:100;box-sizing:border-box;top:calc(100% + 6px);right:0;width:min(440px,calc(100vw - 32px));max-height:min(520px,calc(100vh - 100px));overflow:auto;padding:8px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-base,#20242b));border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));border-radius:12px;box-shadow:var(--dsw-shadow-lv3,0 12px 32px rgba(0,0,0,.45))} .dsh-git-working-dirs__hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:2px 4px 8px} .dsh-git-working-dirs__list{display:flex;flex-direction:column;gap:6px} .dsh-git-working-dirs__directory{display:flex;align-items:center;gap:10px;padding:8px;border-radius:8px} .dsh-git-working-dirs__directory:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))} .dsh-git-working-dirs__details{flex:1;min-width:0;overflow-wrap:anywhere} .dsh-git-working-dirs__name{font-size:13px;font-weight:600;margin-bottom:2px} .dsh-git-working-dirs__path{font:11px var(--dsw-font-mono,monospace);color:var(--dsw-alias-label-secondary)} .dsh-git-working-dirs__menu button{font:inherit;font-size:12px;border:1px solid var(--dsw-alias-border-l2,#666);border-radius:5px;padding:5px 9px;background:var(--dsw-alias-bg-base,#20242b);color:inherit;cursor:pointer;white-space:nowrap} .dsh-git-working-dirs__menu button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#303846)} .dsh-git-working-dirs__menu button:disabled{opacity:.5;cursor:default} .dsh-git-working-dirs__error{color:#ef7878;font-size:12px;margin:7px 4px;overflow-wrap:anywhere} .dsh-git-working-dirs__notice{font-size:12px;margin:7px 4px;color:var(--dsw-alias-label-secondary)} .dsh-git-mutation{margin:8px 0;border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.25));border-radius:8px;padding:12px;min-width:0;color:var(--dsw-alias-label-primary)} .dsh-git-mutation__header{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px} .dsh-git-mutation__title{flex:1;min-width:120px;overflow-wrap:anywhere;font-size:13px;font-weight:600} .dsh-git-mutation__status{font-size:12px;color:var(--dsw-alias-label-secondary)} .dsh-git-mutation[data-state=error] .dsh-git-mutation__status{color:#ef7878} .dsh-git-mutation button{font:inherit;font-size:12px;border:1px solid var(--dsw-alias-border-l2,#666);border-radius:5px;padding:5px 9px;background:var(--dsw-alias-bg-base,#20242b);color:inherit;cursor:pointer} .dsh-git-mutation button:disabled{opacity:.5;cursor:default} .dsh-git-mutation__output{white-space:pre-wrap;overflow-wrap:anywhere;font:12px var(--dsw-font-mono,monospace);margin:10px 0} .dsh-git-mutation__warning{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dsw-alias-state-warning-primary,#d9a84c);font-size:12px;margin:8px 0} .dsh-git-mutation__reason{font-size:12px;color:var(--dsw-alias-label-secondary);margin:8px 0} .dsh-git-mutation__diff{max-height:none} `; function installStyle() { let style = document.getElementById(STYLE_ID); if (!style) { style = document.createElement("style"); style.id = STYLE_ID; document.head.appendChild(style); } style.textContent = STYLE_TEXT; } async function request(path, options) { try { const response = await fetch(path, options); const envelope = await response.json(); if (envelope?.ok === true) return envelope; return { ok: false, error: { message: envelope?.error?.message || "Git workspace request failed." } }; } catch { return { ok: false, error: { message: "Cannot reach the Git workspace route. After installing changes, restart DSH and refresh this page." } }; } } function GitWorkingDirsMenu(props) { const [menuOpen, setMenuOpen] = react.useState(false); const [directories, setDirectories] = react.useState([]); const [loaded, setLoaded] = react.useState(false); const [error, setError] = react.useState(""); const [opening, setOpening] = react.useState(null); const [notice, setNotice] = react.useState(""); const openingRef = react.useRef(false); react.useEffect(() => { let active = true; let fetching = false; const refresh = async () => { if (!active || fetching || document.visibilityState === "hidden") return; fetching = true; try { const result = await props.list(); if (!active) return; setLoaded(true); if (result.ok) setDirectories(result.value.entries || []); else setError(result.error.message); } finally { fetching = false; } }; void refresh(); const timer = window.setInterval(() => { void refresh(); }, 4000); return () => { active = false; window.clearInterval(timer); }; }, [props.list]); const openWorkspace = async (entry) => { if (openingRef.current || !entry.ok) return; const directory = entry.workingDirectory || entry.repository; const existingWindow = workspaceWindows.get(directory); if (existingWindow && !existingWindow.closed) { existingWindow.focus(); setNotice(`Focused VS Code for ${directory}`); return; } const editorWindow = window.open("about:blank", "_blank"); if (!editorWindow) { setError("Allow popups for DSH, then click Open VS Code again. No editor was started."); return; } openingRef.current = true; setOpening(directory); setError(""); setNotice("Starting the shared editor server if needed…"); try { const result = await props.open(entry.id); if (!result.ok) throw new Error(result.error.message); if (editorWindow.closed) { setNotice("The editor tab was closed. Click Open VS Code to reopen it."); return; } const editorUrl = new URL(result.value.url); if (editorUrl.protocol !== "http:" || editorUrl.hostname !== "127.0.0.1") { throw new Error("The editor returned an unexpected non-local URL."); } editorWindow.location.href = editorUrl.href; workspaceWindows.set(directory, editorWindow); setNotice("VS Code opened in another tab. Select Source Control (Ctrl+Shift+G) to view Git changes."); } catch (error) { editorWindow.close(); setError(error.message || "Unable to open VS Code."); setNotice(""); } finally { openingRef.current = false; setOpening(null); } }; return jsx.jsxs("div", { className: "dsh-git-working-dirs", children: [ jsx.jsx("button", { type: "button", className: "dsh-git-working-dirs__trigger", "aria-haspopup": "menu", "aria-expanded": menuOpen, onClick: () => setMenuOpen(current => !current), children: "Git Working Dirs" }), menuOpen ? jsx.jsx("button", { type: "button", className: "dsh-git-working-dirs__backdrop", "aria-label": "Close Git Working Dirs", onClick: () => setMenuOpen(false) }) : null, menuOpen ? jsx.jsxs("div", { className: "dsh-git-working-dirs__menu", role: "menu", children: [ jsx.jsx("p", { className: "dsh-git-working-dirs__hint", children: "Tracked directories. VS Code starts only when you click Open VS Code." }), error ? jsx.jsx("div", { className: "dsh-git-working-dirs__error", role: "alert", children: error }) : null, notice ? jsx.jsx("div", { className: "dsh-git-working-dirs__notice", role: "status", children: notice }) : null, !loaded ? jsx.jsx("p", { className: "dsh-git-working-dirs__hint", children: "Loading tracked directories…" }) : null, loaded && directories.length === 0 ? jsx.jsx("p", { className: "dsh-git-working-dirs__hint", children: "No Git directories tracked yet. The agent adds them with add_touched_git_directory." }) : null, jsx.jsx("div", { className: "dsh-git-working-dirs__list", children: directories.map(entry => { const directory = entry.workingDirectory || entry.repository || entry.path; return jsx.jsxs("div", { className: "dsh-git-working-dirs__directory", role: "menuitem", children: [ jsx.jsxs("div", { className: "dsh-git-working-dirs__details", children: [ jsx.jsx("div", { className: "dsh-git-working-dirs__name", children: directory.split("/").filter(Boolean).pop() || directory }), jsx.jsx("div", { className: "dsh-git-working-dirs__path", children: directory }), !entry.ok ? jsx.jsx("div", { className: "dsh-git-working-dirs__error", children: entry.error?.message || "Directory is unavailable." }) : null, ] }), jsx.jsx("button", { type: "button", disabled: !entry.ok || opening !== null, onClick: () => { void openWorkspace(entry); }, children: opening === directory ? "Opening…" : "Open VS Code" }), ] }, entry.id); }) }), ] }) : null, ] }); } function mutationDiffs(block) { if ("kind" in block && (block.isError || block.error?.code === "interrupted")) return null; const view = "kind" in block ? block.resultView : block.callView; if (view?.card !== "diff" || !Array.isArray(view.diffs) || view.diffs.length === 0) return null; if (!view.diffs.every(diff => diff && typeof diff.path === "string" && (diff.oldText === null || typeof diff.oldText === "string") && typeof diff.newText === "string")) return null; return view.diffs; } function mutationOutput(block) { if (!("kind" in block)) return ""; const text = (Array.isArray(block.content) ? block.content : []).filter(part => part?.type === "text" && typeof part.text === "string").map(part => part.text).join("\n"); if (text) return text; return block.error?.message || [block.error?.name, block.error?.code].filter(Boolean).join(": "); } function mutationDisplayPath(block, diffs) { if (diffs) return diffs[0].path; try { const args = JSON.parse(("kind" in block ? block.call?.argsRaw : block.argsRaw) || "{}"); return typeof args.file_path === "string" ? args.file_path : ""; } catch { return ""; } } function localEditorUrl(value) { const url = new URL(value); if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password) { throw new Error("The editor returned an unexpected non-local URL."); } return url.href; } function MutationToolCard(props) { const block = props.block; const settled = "kind" in block; const failed = settled && (block.isError || block.error?.code === "interrupted"); const state = !settled ? "running" : block.error?.code === "interrupted" ? "stopped" : failed ? "error" : "ok"; const status = { running: "Running", stopped: "Interrupted", error: "Error", ok: "Success" }[state]; const diffs = mutationDiffs(block); const output = mutationOutput(block); const displayPath = mutationDisplayPath(block, diffs); const [expanded, setExpanded] = react.useState(true); const [record, setRecord] = react.useState(null); const [error, setError] = react.useState(""); const [notice, setNotice] = react.useState(""); const [opening, setOpening] = react.useState(false); const openingRef = react.useRef(false); react.useEffect(() => { let active = true; setRecord(null); setError(""); setNotice(""); if (settled) { void props.mutation(props.callId).then(result => { if (!active) return; setRecord({ callId: props.callId, ...(result.ok ? { value: result.value } : { reason: result.error?.message || "Mutation metadata is unavailable." }) }); }).catch(() => { if (active) setRecord({ callId: props.callId, reason: "Mutation metadata is unavailable." }); }); } return () => { active = false; }; }, [props.callId, settled, props.mutation]); const metadata = record?.callId === props.callId ? record.value : null; const canOpen = settled && !failed && metadata?.canOpen === true && typeof metadata.workspaceDirectory === "string" && metadata.workspaceDirectory !== "" && typeof metadata.filePath === "string" && metadata.filePath !== ""; const disabledReason = !settled ? "The tool is still running. Files can be opened only after a successful recorded mutation." : failed ? "This call did not succeed. No successful mutation is available to open." : !record || record.callId !== props.callId ? "Loading recorded mutation metadata…" : !canOpen ? record.reason || metadata?.reason || "No successful mutation record is available for this call. Historical calls cannot be opened from this card." : ""; const openMutation = async () => { if (!canOpen || openingRef.current) return; const directory = metadata.workspaceDirectory; const remembered = workspaceWindows.get(directory); const existing = remembered && !remembered.closed ? remembered : null; let popup = null; if (existing) { try { existing.focus(); } catch { setError("The browser could not focus the existing VS Code window. It was left unchanged; switch to its tab and try again."); return; } } else { popup = window.open("about:blank", "_blank"); if (!popup) { setError("Allow popups for DSH, then click Open in VS Code again. No editor request was sent."); return; } } openingRef.current = true; setOpening(true); setError(""); setNotice(existing ? "Opening the file in the existing VS Code workspace…" : "Opening the recorded file in VS Code…"); try { const result = await props.openMutation(props.callId, Boolean(existing)); if (!result.ok) throw new Error(result.error?.message || "Unable to open the recorded file."); const delivery = result.value; if (existing) { if (delivery.delivery !== "existing") throw new Error("The existing editor could not receive the file. Its window was left unchanged."); setNotice("File opened in the existing VS Code workspace without reloading it."); } else if (delivery.delivery === "existing") { popup.close(); setNotice("File opened in an existing VS Code workspace. Switch to its tab; DSH no longer has a window reference to focus it."); } else if (delivery.delivery === "new") { const url = localEditorUrl(delivery.url); if (typeof delivery.directory !== "string" || !delivery.directory) throw new Error("The editor returned no workspace directory."); if (popup.closed) { setNotice("The reserved tab was closed. Click Open in VS Code again to open the file."); return; } popup.location.href = url; workspaceWindows.set(delivery.directory, popup); setNotice("File opened in a new VS Code workspace tab."); } else { throw new Error("The editor returned an unknown file-delivery mode."); } if (delivery.warning) setNotice(current => `${current}\n${delivery.warning}`); } catch (failure) { if (popup && !popup.closed) popup.close(); setError(failure.message || "Unable to open VS Code."); setNotice(""); } finally { openingRef.current = false; setOpening(false); } }; return jsx.jsxs("section", { className: "dsh-git-mutation", "data-tool": props.toolName, "data-state": state, children: [ jsx.jsxs("header", { className: "dsh-git-mutation__header", children: [ jsx.jsx("span", { className: "dsh-git-mutation__title", children: `${props.toolName === "edit" ? "Edit" : "Write"}${displayPath ? ` · ${displayPath}` : ""}` }), jsx.jsx("span", { className: "dsh-git-mutation__status", role: "status", children: status }), diffs ? jsx.jsx("button", { type: "button", "aria-expanded": expanded, onClick: () => setExpanded(current => !current), children: expanded ? "Collapse diff" : "Expand diff" }) : null, typeof props.inspect === "function" ? jsx.jsx("button", { type: "button", onClick: props.inspect, children: "Inspect" }) : null, jsx.jsx("button", { type: "button", disabled: !canOpen || opening, title: disabledReason || undefined, onClick: () => { void openMutation(); }, children: opening ? "Opening…" : "Open in VS Code" }), jsx.jsx("button", { type: "button", disabled: !canOpen || opening, title: disabledReason || undefined, onClick: () => { if (canOpen) props.openFile(metadata.filePath); }, children: "Open" }), ] }), diffs && expanded ? jsx.jsxs("div", { children: [ jsx.jsx("div", { className: "dsh-git-mutation__reason", children: settled ? "Applied changes" : "Proposed changes · tool is still running" }), jsx.jsx(DiffViewer, { diffs, viewMode: "split", maxLines: Infinity, className: "dsh-git-mutation__diff" }), ] }) : null, output ? jsx.jsx("pre", { className: failed ? "dsh-git-mutation__output dsh-git-workspaces__error" : "dsh-git-mutation__output", children: output }) : null, metadata?.warning ? jsx.jsx("div", { className: "dsh-git-mutation__warning", children: metadata.warning }) : null, disabledReason ? jsx.jsx("div", { className: "dsh-git-mutation__reason", children: disabledReason }) : null, error ? jsx.jsx("div", { className: "dsh-git-workspaces__error", role: "alert", children: error }) : null, notice ? jsx.jsx("div", { className: "dsh-git-mutation__output", role: "status", children: notice }) : null, ] }); } function apply(context) { installStyle(); context.slots.inject("conversation.session.header.actions", () => context.slots.register({ name: "conversation.session.header.actions", id: "touched-git-working-dirs", order: 10000, inject: (sessionId) => { const query = `sessionId=${encodeURIComponent(sessionId)}`; return { list: () => request(`/touched-git/list?${query}`), open: (path) => request(`/touched-git/editor/open?${query}&path=${encodeURIComponent(path)}`, { method: "POST", headers: { "x-dsh-editor-action": "open" }, }), }; }, }, GitWorkingDirsMenu)); context.slots.inject("tool.call.toolview", function* () { for (const key of ["write", "edit"]) { yield context.slots.register({ name: "tool.call.toolview", key, priority: -10, inject: (sessionId) => { const query = `sessionId=${encodeURIComponent(sessionId)}`; return { mutation: (callId) => request(`/touched-git/mutation?${query}&callId=${encodeURIComponent(callId)}`), openMutation: (callId, requireExisting) => request(`/touched-git/editor/open-file?${query}&callId=${encodeURIComponent(callId)}${requireExisting ? "&requireExisting=true" : ""}`, { method: "POST", headers: { "x-dsh-editor-action": "open" }, }), }; }, }, MutationToolCard); } }); } return { apply, inject: ["slots"] }; }, });