@linxin666/dsh-remote-web-ui 0.2.1 → 0.2.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"UpdateEntry.d.ts","sourceRoot":"","sources":["../../../src/client/UpdateEntry.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAOnE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA2GxD"}
1
+ {"version":3,"file":"UpdateEntry.d.ts","sourceRoot":"","sources":["../../../src/client/UpdateEntry.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAOnE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA8GxD"}
@@ -2,8 +2,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  /**
3
3
  * The sidebar update seat: the download trigger beside the remote-control
4
4
  * trigger plus the update panel modal. Owns the flow — probe the registry
5
- * on open, auto-run the update when a newer release exists, report the
6
- * outcome (restart hint on success, translated failure on error).
5
+ * on open, show the result, and let the user start the update from the
6
+ * result view (#507), then report the outcome (restart hint on success,
7
+ * translated failure on error).
7
8
  * Component-local state per the client stack rules.
8
9
  */
9
10
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -65,9 +66,11 @@ export function UpdateEntry({ wide, t }) {
65
66
  setView({ kind: "result", status });
66
67
  return;
67
68
  }
69
+ // Checking never starts an update by itself (#507): the result view
70
+ // carries the confirmation button that calls startUpdate.
68
71
  setView({ kind: "result", status });
69
- // Auto-update: an npm install with a newer release proceeds without a
70
- // second confirmation — clicking the update trigger is the intent.
72
+ }, [t]);
73
+ const startUpdate = useCallback(async (status) => {
71
74
  if (status.mode !== "npm" || !status.outdated)
72
75
  return;
73
76
  setView({ kind: "updating", status });
@@ -105,5 +108,5 @@ export function UpdateEntry({ wide, t }) {
105
108
  };
106
109
  }, [probeAvailability]);
107
110
  const updateLabel = updateAvailable ? t("update.availableLabel") : t("update.label");
108
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : "rail", "data-update-available": updateAvailable ? "true" : undefined, "aria-label": updateLabel, title: updateLabel, onClick: openPanel, children: _jsx(IconDownloadOutline16, { size: wide ? 16 : 18 }) }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(UpdatePanel, { t: t, view: view, onClose: closePanel, onRecheck: () => { void check(); } })] })), document.body)] }));
111
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : "rail", "data-update-available": updateAvailable ? "true" : undefined, "aria-label": updateLabel, title: updateLabel, onClick: openPanel, children: _jsx(IconDownloadOutline16, { size: wide ? 16 : 18 }) }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(UpdatePanel, { t: t, view: view, onClose: closePanel, onRecheck: () => { void check(); }, onStartUpdate: (status) => { void startUpdate(status); } })] })), document.body)] }));
109
112
  }
@@ -30,11 +30,13 @@ export interface UpdatePanelProps {
30
30
  onClose(): void;
31
31
  /** Re-run the check from a terminal state. */
32
32
  onRecheck(): void;
33
+ /** Start the update from the result view (the confirmation step, #507). */
34
+ onStartUpdate(status: UpdateStatus): void;
33
35
  }
34
36
  /**
35
37
  * Render the update panel.
36
38
  * @param props - copy, view state, and actions.
37
39
  * @returns the panel element tree.
38
40
  */
39
- export declare function UpdatePanel({ t, view, onClose, onRecheck }: UpdatePanelProps): import("react").JSX.Element;
41
+ export declare function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }: UpdatePanelProps): import("react").JSX.Element;
40
42
  //# sourceMappingURL=UpdatePanel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"UpdatePanel.d.ts","sourceRoot":"","sources":["../../../src/client/UpdatePanel.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGjE,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,IAAI,IAAI,CAAA;IACf,8CAA8C;IAC9C,SAAS,IAAI,IAAI,CAAA;CAClB;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,gBAAgB,+BA0C5E"}
1
+ {"version":3,"file":"UpdatePanel.d.ts","sourceRoot":"","sources":["../../../src/client/UpdatePanel.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGjE,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,IAAI,IAAI,CAAA;IACf,8CAA8C;IAC9C,SAAS,IAAI,IAAI,CAAA;IACjB,2EAA2E;IAC3E,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;CAC1C;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,gBAAgB,+BAiD3F"}
@@ -14,11 +14,11 @@ function anchorLatest(status) {
14
14
  * @param props - copy, view state, and actions.
15
15
  * @returns the panel element tree.
16
16
  */
17
- export function UpdatePanel({ t, view, onClose, onRecheck }) {
17
+ export function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }) {
18
18
  const status = view.kind === "result" || view.kind === "updating" ? view.status : undefined;
19
19
  const title = view.kind === "done" && view.result.ok ? t("update.done") : t("update.title");
20
20
  const subtitle = subtitleOf(t, view);
21
- return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": title, children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: title }), subtitle !== undefined && _jsx("p", { className: css.subtitle, children: subtitle })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t("update.close"), onClick: onClose, children: _jsx(IconCloseOutline16, {}) })] }), view.kind === "checking" && _jsx("p", { className: css.updateStatus, children: t("update.checking") }), view.kind === "result" && status !== undefined && _jsx(ResultBody, { t: t, status: status }), view.kind === "updating" && status !== undefined && (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" }) }), _jsx("p", { className: css.updateDetail, children: t("update.cooldownNotice") }), _jsx(PackageList, { status: status })] })), view.kind === "done" && _jsx(DoneBody, { t: t, result: view.result }), view.kind === "error" && (_jsxs("div", { children: [_jsx("p", { className: css.updateError, children: view.message }), view.detail !== undefined && _jsx("pre", { className: css.updateOutput, children: view.detail })] })), (view.kind === "done" || view.kind === "error") && (_jsx("div", { className: css.updateActions, children: _jsxs("button", { type: "button", className: css.updateRetry, onClick: onRecheck, children: [_jsx(IconRefreshOutline16, {}), " ", t("update.retry")] }) }))] }));
21
+ return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": title, children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: title }), subtitle !== undefined && _jsx("p", { className: css.subtitle, children: subtitle })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t("update.close"), onClick: onClose, children: _jsx(IconCloseOutline16, {}) })] }), view.kind === "checking" && _jsx("p", { className: css.updateStatus, children: t("update.checking") }), view.kind === "result" && status !== undefined && _jsx(ResultBody, { t: t, status: status }), view.kind === "result" && status !== undefined && status.mode === "npm" && status.outdated && status.error === undefined && (_jsx("div", { className: css.updateActions, children: _jsx("button", { type: "button", className: css.updateRetry, onClick: () => onStartUpdate(status), children: t("update.start") }) })), view.kind === "updating" && status !== undefined && (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" }) }), _jsx("p", { className: css.updateDetail, children: t("update.cooldownNotice") }), _jsx(PackageList, { status: status })] })), view.kind === "done" && _jsx(DoneBody, { t: t, result: view.result }), view.kind === "error" && (_jsxs("div", { children: [_jsx("p", { className: css.updateError, children: view.message }), view.detail !== undefined && _jsx("pre", { className: css.updateOutput, children: view.detail })] })), (view.kind === "done" || view.kind === "error") && (_jsx("div", { className: css.updateActions, children: _jsxs("button", { type: "button", className: css.updateRetry, onClick: onRecheck, children: [_jsx(IconRefreshOutline16, {}), " ", t("update.retry")] }) }))] }));
22
22
  }
23
23
  /** The subtitle copy per view state (absent on plain results). */
24
24
  function subtitleOf(t, view) {
@@ -78,6 +78,7 @@ export declare const zh: {
78
78
  'update.upToDateDetail': string;
79
79
  'update.found': string;
80
80
  'update.foundDetail': string;
81
+ 'update.start': string;
81
82
  'update.updating': string;
82
83
  'update.cooldownNotice': string;
83
84
  'update.updatingTitle': string;
@@ -188,6 +189,7 @@ export declare const en: {
188
189
  'update.upToDateDetail': string;
189
190
  'update.found': string;
190
191
  'update.foundDetail': string;
192
+ 'update.start': string;
191
193
  'update.updating': string;
192
194
  'update.cooldownNotice': string;
193
195
  'update.updatingTitle': string;
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0GmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0GsB,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2GmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2GsB,CAAA"}
@@ -79,10 +79,11 @@ export const zh = {
79
79
  'update.upToDate': '当前已是最新版本',
80
80
  'update.upToDateDetail': '{name} 已是最新版本 v{version}。',
81
81
  'update.found': '发现新版本',
82
- 'update.foundDetail': '{name} 有新版本 v{version},正在自动更新…',
82
+ 'update.foundDetail': '{name} 有新版本 v{version}。确认后开始更新。',
83
+ 'update.start': '开始更新',
83
84
  'update.updating': '正在更新 {name} 至 v{version},可能需要 1-2 分钟…',
84
85
  'update.cooldownNotice': '本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。',
85
- 'update.updatingTitle': '正在自动更新…',
86
+ 'update.updatingTitle': '正在更新…',
86
87
  'update.done': '更新完成',
87
88
  'update.doneDetail': '所有组件已更新到最新版本。',
88
89
  'update.restartHint': '请重启 dsh web 使新版本生效:在终端按 Ctrl+C 停止后重新运行 dsh web。',
@@ -187,10 +188,11 @@ export const en = {
187
188
  'update.upToDate': 'Everything is up to date',
188
189
  'update.upToDateDetail': '{name} is at the latest version v{version}.',
189
190
  'update.found': 'A new version is available',
190
- 'update.foundDetail': '{name} has a newer release v{version}; updating automatically…',
191
+ 'update.foundDetail': '{name} has a newer release v{version}. Confirm to start the update.',
192
+ 'update.start': 'Update now',
191
193
  'update.updating': 'Updating {name} to v{version}; this may take 1-2 minutes…',
192
194
  'update.cooldownNotice': 'This user-initiated update bypasses pnpm\'s 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.',
193
- 'update.updatingTitle': 'Updating automatically…',
195
+ 'update.updatingTitle': 'Updating…',
194
196
  'update.done': 'Update complete',
195
197
  'update.doneDetail': 'All components are up to date.',
196
198
  'update.restartHint': 'Restart dsh web for the new version to take effect: stop it with Ctrl+C in the terminal, then run dsh web again.',
@@ -83,12 +83,7 @@ export declare function readPairParams(search: string): {
83
83
  pair?: string;
84
84
  workspace?: string;
85
85
  };
86
- /**
87
- * Strip one query parameter from the current URL without reloading.
88
- * @param name - the parameter to remove.
89
- * @returns the new search string ('' when empty).
90
- */
91
- export declare function stripParam(name: string): string; /** Human-readable expiry clock, e.g. "10:35". */
86
+ /** Human-readable expiry clock, e.g. "10:35". */
92
87
  export declare function formatClock(epochMs: number): string;
93
88
  /**
94
89
  * Copy text to the clipboard with a fallback for insecure contexts
@@ -1 +1 @@
1
- {"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAwB;AACxB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAA;IACR,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;AAExG,8BAA8B;AAC9B,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;CAAE,CAAA;AAEjF,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACxC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,cAAc,CAAA;IAC5E,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB9F;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,aAAa,CAAC,CAUrF;AAED,iEAAiE;AACjE,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAG9C;AAED,mFAAmF;AACnF,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAQpF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C,CAAA,iDAAiD;AAClD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB7D"}
1
+ {"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAwB;AACxB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAA;IACR,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;AAExG,8BAA8B;AAC9B,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;CAAE,CAAA;AAEjF,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACxC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,cAAc,CAAA;IAC5E,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB9F;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,aAAa,CAAC,CAUrF;AAED,iEAAiE;AACjE,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAG9C;AAED,mFAAmF;AACnF,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAQpF;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB7D"}
@@ -74,16 +74,7 @@ export function readPairParams(search) {
74
74
  ...(workspace !== null && workspace !== '' ? { workspace } : {}),
75
75
  };
76
76
  }
77
- /**
78
- * Strip one query parameter from the current URL without reloading.
79
- * @param name - the parameter to remove.
80
- * @returns the new search string ('' when empty).
81
- */
82
- export function stripParam(name) {
83
- const url = new URL(window.location.href);
84
- url.searchParams.delete(name);
85
- return url.search;
86
- } /** Human-readable expiry clock, e.g. "10:35". */
77
+ /** Human-readable expiry clock, e.g. "10:35". */
87
78
  export function formatClock(epochMs) {
88
79
  const date = new Date(epochMs);
89
80
  const hours = String(date.getHours()).padStart(2, '0');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
3
  "description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -78,7 +78,6 @@
78
78
  "@types/react": "~18.3.1",
79
79
  "@types/react-dom": "~18.3.0",
80
80
  "jsdom": "29.1.1",
81
- "lightningcss": "^1.32.0",
82
81
  "react": "^18.2.0",
83
82
  "react-dom": "^18.2.0",
84
83
  "tsdown": "^0.22.2",
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The sidebar update seat: the download trigger beside the remote-control
3
3
  * trigger plus the update panel modal. Owns the flow — probe the registry
4
- * on open, auto-run the update when a newer release exists, report the
5
- * outcome (restart hint on success, translated failure on error).
4
+ * on open, show the result, and let the user start the update from the
5
+ * result view (#507), then report the outcome (restart hint on success,
6
+ * translated failure on error).
6
7
  * Component-local state per the client stack rules.
7
8
  */
8
9
  import { useCallback, useEffect, useRef, useState } from "react"
@@ -72,9 +73,12 @@ export function UpdateEntry({ wide, t }: UpdateEntryProps) {
72
73
  setView({ kind: "result", status })
73
74
  return
74
75
  }
76
+ // Checking never starts an update by itself (#507): the result view
77
+ // carries the confirmation button that calls startUpdate.
75
78
  setView({ kind: "result", status })
76
- // Auto-update: an npm install with a newer release proceeds without a
77
- // second confirmation — clicking the update trigger is the intent.
79
+ }, [t])
80
+
81
+ const startUpdate = useCallback(async (status: UpdateStatus): Promise<void> => {
78
82
  if (status.mode !== "npm" || !status.outdated) return
79
83
  setView({ kind: "updating", status })
80
84
  const token = ++runToken.current
@@ -128,7 +132,7 @@ export function UpdateEntry({ wide, t }: UpdateEntryProps) {
128
132
  {open && createPortal((
129
133
  <div className={css.overlay} role="presentation">
130
134
  <div className={css.mask} aria-hidden="true" onClick={closePanel} />
131
- <UpdatePanel t={t} view={view} onClose={closePanel} onRecheck={() => { void check() }} />
135
+ <UpdatePanel t={t} view={view} onClose={closePanel} onRecheck={() => { void check() }} onStartUpdate={(status) => { void startUpdate(status) }} />
132
136
  </div>
133
137
  ), document.body)}
134
138
  </>
@@ -24,6 +24,8 @@ export interface UpdatePanelProps {
24
24
  onClose(): void
25
25
  /** Re-run the check from a terminal state. */
26
26
  onRecheck(): void
27
+ /** Start the update from the result view (the confirmation step, #507). */
28
+ onStartUpdate(status: UpdateStatus): void
27
29
  }
28
30
 
29
31
  /** The anchor package name (aggregate first) for copy purposes. */
@@ -41,7 +43,7 @@ function anchorLatest(status: UpdateStatus | undefined): string | undefined {
41
43
  * @param props - copy, view state, and actions.
42
44
  * @returns the panel element tree.
43
45
  */
44
- export function UpdatePanel({ t, view, onClose, onRecheck }: UpdatePanelProps) {
46
+ export function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }: UpdatePanelProps) {
45
47
  const status = view.kind === "result" || view.kind === "updating" ? view.status : undefined
46
48
  const title = view.kind === "done" && view.result.ok ? t("update.done") : t("update.title")
47
49
  const subtitle = subtitleOf(t, view)
@@ -58,6 +60,13 @@ export function UpdatePanel({ t, view, onClose, onRecheck }: UpdatePanelProps) {
58
60
  </div>
59
61
  {view.kind === "checking" && <p className={css.updateStatus}>{t("update.checking")}</p>}
60
62
  {view.kind === "result" && status !== undefined && <ResultBody t={t} status={status} />}
63
+ {view.kind === "result" && status !== undefined && status.mode === "npm" && status.outdated && status.error === undefined && (
64
+ <div className={css.updateActions}>
65
+ <button type="button" className={css.updateRetry} onClick={() => onStartUpdate(status)}>
66
+ {t("update.start")}
67
+ </button>
68
+ </div>
69
+ )}
61
70
  {view.kind === "updating" && status !== undefined && (
62
71
  <div>
63
72
  <p className={css.updateStatus}>
@@ -80,10 +80,11 @@ export const zh = {
80
80
  'update.upToDate': '当前已是最新版本',
81
81
  'update.upToDateDetail': '{name} 已是最新版本 v{version}。',
82
82
  'update.found': '发现新版本',
83
- 'update.foundDetail': '{name} 有新版本 v{version},正在自动更新…',
83
+ 'update.foundDetail': '{name} 有新版本 v{version}。确认后开始更新。',
84
+ 'update.start': '开始更新',
84
85
  'update.updating': '正在更新 {name} 至 v{version},可能需要 1-2 分钟…',
85
86
  'update.cooldownNotice': '本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。',
86
- 'update.updatingTitle': '正在自动更新…',
87
+ 'update.updatingTitle': '正在更新…',
87
88
  'update.done': '更新完成',
88
89
  'update.doneDetail': '所有组件已更新到最新版本。',
89
90
  'update.restartHint': '请重启 dsh web 使新版本生效:在终端按 Ctrl+C 停止后重新运行 dsh web。',
@@ -195,10 +196,11 @@ export const en = {
195
196
  'update.upToDate': 'Everything is up to date',
196
197
  'update.upToDateDetail': '{name} is at the latest version v{version}.',
197
198
  'update.found': 'A new version is available',
198
- 'update.foundDetail': '{name} has a newer release v{version}; updating automatically…',
199
+ 'update.foundDetail': '{name} has a newer release v{version}. Confirm to start the update.',
200
+ 'update.start': 'Update now',
199
201
  'update.updating': 'Updating {name} to v{version}; this may take 1-2 minutes…',
200
202
  'update.cooldownNotice': 'This user-initiated update bypasses pnpm\'s 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.',
201
- 'update.updatingTitle': 'Updating automatically…',
203
+ 'update.updatingTitle': 'Updating…',
202
204
  'update.done': 'Update complete',
203
205
  'update.doneDetail': 'All components are up to date.',
204
206
  'update.restartHint': 'Restart dsh web for the new version to take effect: stop it with Ctrl+C in the terminal, then run dsh web again.',
@@ -128,16 +128,7 @@ export function readPairParams(search: string): { pair?: string; workspace?: str
128
128
  }
129
129
  }
130
130
 
131
- /**
132
- * Strip one query parameter from the current URL without reloading.
133
- * @param name - the parameter to remove.
134
- * @returns the new search string ('' when empty).
135
- */
136
- export function stripParam(name: string): string {
137
- const url = new URL(window.location.href)
138
- url.searchParams.delete(name)
139
- return url.search
140
- }/** Human-readable expiry clock, e.g. "10:35". */
131
+ /** Human-readable expiry clock, e.g. "10:35". */
141
132
  export function formatClock(epochMs: number): string {
142
133
  const date = new Date(epochMs)
143
134
  const hours = String(date.getHours()).padStart(2, '0')
@@ -2,7 +2,7 @@
2
2
  /** mobile-theme: light default, persisted dark toggle, document wiring. */
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
4
  import {
5
- getMobileTheme, initMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme,
5
+ getMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme,
6
6
  } from './mobile-theme.ts'
7
7
 
8
8
  const STORAGE_KEY = 'dsh.remote.theme'