//#region dsh-start&exit 浏览器客户端 bundle
/**
* dsh-start&exit 客户端:
* - 在 dsh 设置菜单中注册一个始终排在最后的“启动与退出”栏目
* - 提供对齐展示的启动、退出与重新启动操作;确认后分别调用
* remote.dshStartExit.start() / remote.dshStartExit.exit() /
* remote.dshStartExit.restart()
* - 重启成功后等待后台服务恢复并自动刷新页面
*
* 页面使用 dsh 已提供的 React、slots 和 locale 运行时,不引入新的运行时依赖。
*/
window.__ModuleLoader__.load({
// The loader key follows the resolved npm package name, including its scope.
id: "@kesike/dsh-start-and-exit",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const React = require("react");
const h = React.createElement;
/** 与 archive-manager「删除会话」相同的错误红(设计令牌)。 */
const ERROR_RED = "var(--dsw-alias-state-error-primary)";
const START_BLUE = "var(--dsw-alias-state-business-primary)";
const RESTART_BLUE = START_BLUE;
/** Lucide power 图标(ISC 许可,免费),16px 线性风格贴合现有按钮。 */
const EXIT_ICON_SVG = '';
const START_ICON_SVG = '';
const RESTART_ICON_SVG = '';
const CSS_TEXT = [
".dshExitSettings{box-sizing:border-box;max-width:720px;padding:28px 32px 40px;color:var(--dsw-alias-label-primary)}",
".dshExitSettingsHeader{display:flex;align-items:flex-start;gap:14px;padding-bottom:22px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
".dshExitSettingsIcon{box-sizing:border-box;width:38px;height:38px;display:grid;place-items:center;flex:none;border:1px solid " + ERROR_RED + ";border-radius:12px;color:" + ERROR_RED + ";background:var(--dsw-alias-button-elevated-fill)}",
".dshExitSettingsIcon svg{width:20px;height:20px}",
".dshExitSettingsTitle{margin:0;font-size:20px;line-height:28px;font-weight:650;color:var(--dsw-alias-label-primary)}",
".dshExitSettingsDescription{margin:6px 0 0;max-width:560px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
".dshExitSettingsActions{display:flex;flex-direction:column;gap:12px;margin-top:24px}",
".dshExitSettingsCard{display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:14px;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}",
".dshExitActionIcon{box-sizing:border-box;width:34px;height:34px;display:grid;place-items:center;border:1px solid currentColor;border-radius:10px;background:var(--dsw-alias-button-elevated-fill)}",
".dshExitActionIcon--exit{color:" + ERROR_RED + "}",
".dshExitActionIcon--start{color:" + START_BLUE + "}",
".dshExitActionIcon--restart{color:" + RESTART_BLUE + "}",
".dshExitActionIcon svg{width:18px;height:18px}",
".dshExitSettingsCopy{min-width:0}",
".dshExitSettingsActionTitle{margin:0 0 3px;font-size:14px;line-height:20px;font-weight:650;color:var(--dsw-alias-label-primary)}",
".dshExitSettingsWarning{margin:0;max-width:430px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
".dshExitSettingsButton{display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:none;min-width:112px;height:36px;padding:0 16px;cursor:pointer;border:0;border-radius:10px;color:#fff;font-size:13px;font-weight:650;transition:filter .12s ease,opacity .12s ease}",
".dshExitSettingsButton--exit{background:" + ERROR_RED + "}",
".dshExitSettingsButton--start{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
".dshExitSettingsButton--restart{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
".dshExitSettingsButton svg{width:16px;height:16px}",
".dshExitSettingsButton:hover:not(:disabled){filter:brightness(1.08)}",
".dshExitSettingsButton--start:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
".dshExitSettingsButton--restart:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
".dshExitSettingsButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}",
".dshExitSettingsButton:disabled{cursor:default;opacity:.6}",
".dshExitNavCell{color:" + ERROR_RED + " !important}",
".dshExitNavCell .dshExitNavLabel{color:" + ERROR_RED + " !important;font-weight:650}",
".dshExitNavIcon{box-sizing:border-box;width:16px;height:16px;display:grid;place-items:center;flex:none;color:" + ERROR_RED + "}",
".dshExitNavIcon svg{width:16px;height:16px}",
".dshExitOverlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;background:rgba(15,20,25,.45);animation:dshExitFade .12s ease-out}",
"@keyframes dshExitFade{from{opacity:0}}",
".dshExitModal{box-sizing:border-box;width:min(380px,calc(100vw - 48px));border-radius:16px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);box-shadow:0 24px 64px rgba(0,0,0,.35);padding:24px;display:flex;flex-direction:column;gap:12px;animation:dshExitPop .14s ease-out}",
"@keyframes dshExitPop{from{opacity:0;transform:translateY(6px) scale(.98)}}",
".dshExitTitle{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary)}",
".dshExitBody{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
".dshExitError{margin:0;min-height:0;font-size:12px;line-height:18px;color:" + ERROR_RED + "}",
".dshExitActions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}",
".dshExitCancel{cursor:pointer;height:32px;padding:0 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}",
".dshExitCancel:hover{background:var(--dsw-alias-interactive-bg-hover)}",
".dshExitCancel:disabled{cursor:default;opacity:.6}",
".dshExitConfirm{cursor:pointer;height:32px;padding:0 14px;border-radius:10px;border:none;background:" + ERROR_RED + ";color:#fff;font-size:13px;font-weight:600}",
".dshExitConfirm--start{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
".dshExitConfirm--restart{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
".dshExitConfirm:hover:not(:disabled){filter:brightness(1.08)}",
".dshExitConfirm--start:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
".dshExitConfirm--restart:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
".dshExitConfirm:disabled{cursor:default;opacity:.6}",
"@media (max-width:620px){.dshExitSettings{padding:24px 20px 32px}.dshExitSettingsCard{grid-template-columns:34px minmax(0,1fr)}.dshExitSettingsButton{grid-column:1 / -1;justify-self:start}}"
].join("\n");
/** 客户端严格编解码 shim:与服务端 exitedSchema 对应。 */
const exitedClientSchema = {
parse(value) {
if (value !== true) throw new TypeError("exit must return true");
return value;
}
};
/** 客户端 Remote 描述符:挂载后 ctx.get("remote.dshStartExit") 可用。 */
const START_EXIT_REMOTE = {
package: "@kesike/dsh-start-and-exit",
descriptors: [
{
id: "@kesike/dsh-start-and-exit#dshStartExit/start",
service: "dshStartExit",
namespace: "dshStartExit",
method: "start",
invocation: { kind: "direct" },
parameters: [],
result: {
mode: "strict",
typeSymbol: "@kesike/dsh-start-and-exit/types#Started",
schema: exitedClientSchema
}
},
{
id: "@kesike/dsh-start-and-exit#dshStartExit/exit",
service: "dshStartExit",
namespace: "dshStartExit",
method: "exit",
invocation: { kind: "direct" },
parameters: [],
result: {
mode: "strict",
typeSymbol: "@kesike/dsh-start-and-exit/types#Exited",
schema: exitedClientSchema
}
},
{
id: "@kesike/dsh-start-and-exit#dshStartExit/restart",
service: "dshStartExit",
namespace: "dshStartExit",
method: "restart",
invocation: { kind: "direct" },
parameters: [],
result: {
mode: "strict",
typeSymbol: "@kesike/dsh-start-and-exit/types#Restarted",
schema: exitedClientSchema
}
}
]
};
const NS = "dsh-start-and-exit";
const DICT = {
zh: {
nav: "启动与退出",
title: "启动、退出与重新启动 dsh",
description: "从这里通过可见的 Windows Terminal 安全启动、结束或重新启动 dsh 宿主进程。",
startActionTitle: "启动 dsh",
startWarning: "从 Windows Terminal 的 PowerShell 7 启动 dsh web profile;当前服务会先释放端口。桌面 Chrome 应用快捷方式仍需单独配置启动器。",
startButton: "启动 dsh",
exitActionTitle: "结束 dsh",
exitWarning: "确认后将结束所有运行中的会话并停止后台服务,当前 dsh Web 端口 {endpoint} 会随之释放。",
exitButton: "确认退出",
restartActionTitle: "重新启动 dsh",
restartWarning: "重新启动会短暂中断所有运行中的会话;后台服务恢复后,页面将自动刷新。",
restartButton: "重新启动",
dialogTitle: "退出 DeepSeek Harness?",
dialogBody: "退出后将结束所有运行中的会话并停止后台服务,当前 dsh Web 端口 {endpoint} 会随之释放。",
restartDialogTitle: "重新启动 DeepSeek Harness?",
restartDialogBody: "这会结束当前运行中的会话,后台服务将释放并重新监听 {endpoint}。服务恢复后页面将自动刷新。",
startDialogTitle: "启动 dsh?",
startDialogBody: "这会先结束当前运行中的会话,再在可见的 Windows Terminal(PowerShell 7)中启动 dsh web profile。服务恢复后页面将自动刷新。",
cancel: "取消",
confirm: "确认退出",
restartConfirm: "确认重启",
exiting: "正在退出…",
restarting: "正在重新启动…",
starting: "正在启动 dsh…",
startWaiting: "dsh 正在新的可见 Windows Terminal 中启动,恢复后页面将自动刷新。",
restartWaiting: "后台服务正在重新启动,恢复后页面将自动刷新。",
restartTimeout: "后台服务尚未恢复,请稍后手动刷新页面。",
exitErrorPrefix: "退出失败:",
restartErrorPrefix: "重启失败:",
startErrorPrefix: "启动失败:"
},
en: {
nav: "Start & Exit",
title: "Start, exit, or restart dsh",
description: "Safely start, terminate, or restart the dsh host in a visible Windows Terminal.",
startActionTitle: "Start dsh",
startWarning: "Starts the dsh web profile in PowerShell 7; the current service releases its port first. The desktop Chrome app shortcut still needs a separate launcher configuration.",
startButton: "Start dsh",
exitActionTitle: "Exit dsh",
exitWarning: "Confirming ends all running sessions and stops the background service, releasing the current dsh Web port {endpoint}.",
exitButton: "Confirm exit",
restartActionTitle: "Restart dsh",
restartWarning: "Restarting briefly interrupts all running sessions; this page refreshes automatically after the service recovers.",
restartButton: "Restart",
dialogTitle: "Exit DeepSeek Harness?",
dialogBody: "This ends all running sessions and stops the background service, releasing the current dsh Web port {endpoint}.",
restartDialogTitle: "Restart DeepSeek Harness?",
restartDialogBody: "This ends the current sessions while the background service releases and reclaims {endpoint}. The page refreshes automatically after recovery.",
startDialogTitle: "Start dsh?",
startDialogBody: "This ends the current sessions, then starts the dsh web profile in a visible Windows Terminal (PowerShell 7). The page refreshes after recovery.",
cancel: "Cancel",
confirm: "Confirm exit",
restartConfirm: "Confirm restart",
exiting: "Exiting…",
restarting: "Restarting…",
starting: "Starting dsh…",
startWaiting: "dsh is starting in a new visible Windows Terminal. This page will refresh after it recovers.",
restartWaiting: "The background service is restarting. This page will refresh after it recovers.",
restartTimeout: "The background service has not recovered yet. Please refresh this page later.",
exitErrorPrefix: "Exit failed: ",
restartErrorPrefix: "Restart failed: ",
startErrorPrefix: "Start failed: "
}
};
let overlayEl = null;
let escHandler = null;
let restartPollTimer = null;
function injectCss() {
if (document.querySelector("style[data-plugin-css='dsh-exit']")) return;
const tag = document.createElement("style");
tag.dataset.pluginCss = "dsh-exit";
tag.textContent = CSS_TEXT;
document.head.appendChild(tag);
}
function textOf(t, key, values = {}) {
const raw = typeof t === "function" ? t(key, values) : DICT.zh[key] ?? key;
return String(raw).replace(/\{([A-Za-z0-9_]+)\}/g, (match, name) => values[name] === undefined ? match : String(values[name]));
}
function currentDshEndpoint() {
const location = globalThis.location;
return location?.host || "当前 Web 端口";
}
/** dsh 的设置壳层为未知 section 使用齿轮图标;给本插件的导航项换成 power 图标。 */
function decorateExitNav(t) {
const labels = new Set(["退出", "Exit", "启动与退出", "Start & Exit", textOf(t, "nav")]);
for (const cell of document.querySelectorAll("nav button")) {
const label = [...cell.querySelectorAll("span")].find((candidate) => labels.has(candidate.textContent.trim()));
if (label === undefined) continue;
cell.dataset.dshExitNav = "true";
cell.classList.add("dshExitNavCell");
label.classList.add("dshExitNavLabel");
if (cell.querySelector(".dshExitNavIcon") !== null) continue;
const originalIcon = cell.firstElementChild;
const icon = document.createElement("span");
icon.className = "dshExitNavIcon";
icon.innerHTML = EXIT_ICON_SVG;
if (originalIcon !== null && originalIcon !== label) {
originalIcon.dataset.dshExitOriginalIcon = "true";
originalIcon.style.display = "none";
cell.insertBefore(icon, originalIcon);
} else {
cell.insertBefore(icon, label);
}
}
}
function clearDecoratedExitNav() {
for (const cell of document.querySelectorAll("[data-dsh-exit-nav='true']")) {
cell.querySelector(".dshExitNavIcon")?.remove();
cell.querySelector("[data-dsh-exit-original-icon='true']")?.style.removeProperty("display");
cell.querySelector(".dshExitNavLabel")?.classList.remove("dshExitNavLabel");
cell.classList.remove("dshExitNavCell");
delete cell.dataset.dshExitNav;
}
}
function ExitSettingsSection({ ctx, t }) {
const endpoint = currentDshEndpoint();
const actionCard = ({ action, icon, title, warning, button }) => h("div", {
className: "dshExitSettingsCard",
key: action
}, [
h("span", {
className: "dshExitActionIcon dshExitActionIcon--" + action,
dangerouslySetInnerHTML: { __html: icon },
"aria-hidden": "true",
key: "icon"
}),
h("div", { className: "dshExitSettingsCopy", key: "copy" }, [
h("h3", { className: "dshExitSettingsActionTitle", key: "title" }, textOf(t, title)),
h("p", { className: "dshExitSettingsWarning", key: "warning" }, textOf(t, warning, { endpoint }))
]),
h("button", {
type: "button",
className: "dshExitSettingsButton dshExitSettingsButton--" + action,
"aria-label": textOf(t, button),
onClick: (event) => openModal(ctx, event.currentTarget, t, action),
key: "button"
}, [
h("span", { dangerouslySetInnerHTML: { __html: icon }, "aria-hidden": "true", key: "icon" }),
textOf(t, button)
])
]);
return h("section", {
className: "dshExitSettings",
"aria-labelledby": "dsh-exit-settings-title"
}, [
h("div", { className: "dshExitSettingsHeader", key: "header" }, [
h("span", {
className: "dshExitSettingsIcon",
dangerouslySetInnerHTML: { __html: EXIT_ICON_SVG },
key: "icon"
}),
h("div", { key: "copy" }, [
h("h2", { className: "dshExitSettingsTitle", id: "dsh-exit-settings-title", key: "title" }, textOf(t, "title")),
h("p", { className: "dshExitSettingsDescription", key: "description" }, textOf(t, "description"))
])
]),
h("div", { className: "dshExitSettingsActions", key: "actions" }, [
actionCard({
action: "start",
icon: START_ICON_SVG,
title: "startActionTitle",
warning: "startWarning",
button: "startButton"
}),
actionCard({
action: "exit",
icon: EXIT_ICON_SVG,
title: "exitActionTitle",
warning: "exitWarning",
button: "exitButton"
}),
actionCard({
action: "restart",
icon: RESTART_ICON_SVG,
title: "restartActionTitle",
warning: "restartWarning",
button: "restartButton"
})
])
]);
}
function closeModal() {
if (restartPollTimer !== null) {
clearTimeout(restartPollTimer);
restartPollTimer = null;
}
if (escHandler) {
document.removeEventListener("keydown", escHandler);
escHandler = null;
}
if (overlayEl) {
overlayEl.remove();
overlayEl = null;
}
}
function waitForService({ title, body, errorEl, actions, cancel, confirm, t, pendingKey, waitingKey }) {
const startedAt = Date.now();
let sawUnavailable = false;
title.textContent = textOf(t, pendingKey);
body.textContent = textOf(t, waitingKey);
actions.style.display = "none";
const check = async () => {
try {
const response = await fetch("/?dshRestartProbe=" + Date.now(), { cache: "no-store" });
if (response.ok && (sawUnavailable || Date.now() - startedAt >= 5_000)) {
globalThis.location.reload();
return;
}
} catch {
sawUnavailable = true;
}
if (Date.now() - startedAt >= 60_000) {
restartPollTimer = null;
errorEl.textContent = textOf(t, "restartTimeout");
actions.style.removeProperty("display");
confirm.style.display = "none";
cancel.disabled = false;
return;
}
restartPollTimer = setTimeout(check, 500);
};
restartPollTimer = setTimeout(check, 800);
}
function openModal(ctx, actionBtn, t, action) {
const endpoint = currentDshEndpoint();
const isStart = action === "start";
const isRestart = action === "restart";
const isLaunch = isStart || isRestart;
const keys = isStart ? {
title: "startDialogTitle",
body: "startDialogBody",
confirm: "startButton",
pending: "starting",
button: "startButton",
error: "startErrorPrefix",
waiting: "startWaiting"
} : isRestart ? {
title: "restartDialogTitle",
body: "restartDialogBody",
confirm: "restartConfirm",
pending: "restarting",
button: "restartButton",
error: "restartErrorPrefix",
waiting: "restartWaiting"
} : {
title: "dialogTitle",
body: "dialogBody",
confirm: "confirm",
pending: "exiting",
button: "exitButton",
error: "exitErrorPrefix"
};
const text = (key) => textOf(t, key, { endpoint });
closeModal();
overlayEl = document.createElement("div");
overlayEl.className = "dshExitOverlay";
const panel = document.createElement("div");
panel.className = "dshExitModal";
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-modal", "true");
panel.setAttribute("aria-label", text(keys.title));
const title = document.createElement("h3");
title.className = "dshExitTitle";
title.textContent = text(keys.title);
const body = document.createElement("p");
body.className = "dshExitBody";
body.textContent = text(keys.body);
const errorEl = document.createElement("p");
errorEl.className = "dshExitError";
const actions = document.createElement("div");
actions.className = "dshExitActions";
const cancel = document.createElement("button");
cancel.type = "button";
cancel.className = "dshExitCancel";
cancel.textContent = text("cancel");
const confirm = document.createElement("button");
confirm.type = "button";
confirm.className = "dshExitConfirm" + (isLaunch ? " dshExitConfirm--" + action : "");
confirm.textContent = text(keys.confirm);
actions.append(cancel, confirm);
panel.append(title, body, errorEl, actions);
overlayEl.appendChild(panel);
escHandler = (event) => {
if (event.key === "Escape" && !cancel.disabled) closeModal();
};
document.addEventListener("keydown", escHandler);
overlayEl.addEventListener("click", (event) => {
if (event.target === overlayEl && !cancel.disabled) closeModal();
});
cancel.addEventListener("click", () => {
if (!cancel.disabled) closeModal();
});
confirm.addEventListener("click", () => {
confirm.disabled = true;
cancel.disabled = true;
errorEl.textContent = "";
actionBtn.disabled = true;
actionBtn.setAttribute("aria-label", text(keys.pending));
Promise.resolve()
.then(() => {
const service = ctx.get("remote.dshStartExit");
if (service === undefined) throw new Error("dsh-start&exit 服务不可用(宿主未注册?)");
if (typeof service[action] !== "function") throw new Error("dsh-start&exit 不支持 " + action + " 操作");
return service[action]();
})
.then((result) => {
if (!result || result.ok !== true) {
throw new Error((result && result.error && result.error.message) || action + " 调用失败");
}
if (isLaunch) {
waitForService({
title,
body,
errorEl,
actions,
cancel,
confirm,
t,
pendingKey: keys.pending,
waitingKey: keys.waiting
});
} else {
closeModal();
}
})
.catch((reason) => {
confirm.disabled = false;
cancel.disabled = false;
actionBtn.disabled = false;
actionBtn.setAttribute("aria-label", text(keys.button));
errorEl.textContent = text(keys.error) + (reason instanceof Error ? reason.message : String(reason));
});
});
document.body.appendChild(overlayEl);
}
async function apply(ctx) {
injectCss();
ctx.effect(() => ctx.locale.register(NS, DICT), "dsh-start&exit: settings dictionaries");
const t = ctx.locale.bind(NS);
const decorate = () => decorateExitNav(t);
decorate();
const navObserver = document.body === null ? null : new MutationObserver(decorate);
navObserver?.observe(document.body, { childList: true, subtree: true, characterData: true });
const SettingsSection = (props) => h(ExitSettingsSection, { ...props, ctx });
ctx.slots.inject("settings.section", () => ctx.slots.register({
name: "settings.section",
id: "dsh-start-and-exit",
order: Number.MAX_SAFE_INTEGER,
label: () => t("nav"),
locale: NS
}, SettingsSection));
let disposeRemote = () => {};
const remote = ctx.get("remote");
if (remote !== undefined) disposeRemote = await remote.$mount(START_EXIT_REMOTE);
return async () => {
navObserver?.disconnect();
clearDecoratedExitNav();
closeModal();
disposeRemote();
};
}
exports.inject = ["slots", "locale", "remote"];
exports.apply = apply;
return module.exports;
}
});
//#endregion