Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
357 lines
20 KiB
JavaScript
357 lines
20 KiB
JavaScript
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"] };
|
|
},
|
|
});
|