@matamt-dbg/dsh-studio 0.2.7 → 0.2.8

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.
Files changed (3) hide show
  1. package/client.js +219 -46
  2. package/index.js +147 -4
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -115,8 +115,7 @@ function parsePreviewRequest(value) {
115
115
  var import_react = __toESM(require("react"), 1);
116
116
 
117
117
  // src/ui-styles.ts
118
- var studioStyles = `/* Only this adapter references host tokens. Studio never installs a global theme. */
119
- .studio-ui {
118
+ var adapter = `.studio-ui {
120
119
  --studio-bg: var(--dsw-alias-bg-base);
121
120
  --studio-surface: var(--dsw-alias-bg-layer-1);
122
121
  --studio-inset: var(--dsw-alias-bg-layer-2);
@@ -148,7 +147,9 @@ var studioStyles = `/* Only this adapter references host tokens. Studio never in
148
147
  min-width: 0;
149
148
  overflow-wrap: anywhere;
150
149
  }
151
- .studio-ui *, .studio-ui *::before, .studio-ui *::after { box-sizing: border-box; }
150
+ `;
151
+ var studioStyles = `/* Only this adapter references host tokens. Studio never installs a global theme. */
152
+ ${adapter}.studio-ui *, .studio-ui *::before, .studio-ui *::after { box-sizing: border-box; }
152
153
  .studio-ui .studio-tool-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 12px; }
153
154
  .studio-ui .studio-tool-card { display: flex; flex-direction: column; gap: 8px; padding: 14px; border: 1px solid var(--studio-line); border-radius: var(--studio-radius); cursor: pointer; min-width: 0; }
154
155
  .studio-ui .studio-tool-card[data-selected="true"] { background: var(--studio-hover); border-color: var(--studio-ink); }
@@ -217,6 +218,14 @@ var studioStyles = `/* Only this adapter references host tokens. Studio never in
217
218
  .studio-ui .studio-service-panel[hidden] { display: none; }
218
219
  .studio-ui .studio-service-panel > .studio-ui { max-width: 960px; margin: 0 auto; padding: 24px clamp(16px, 3vw, 40px) 48px; }
219
220
  .studio-ui .studio-layout { display: grid; grid-template-columns: 208px minmax(0, 1fr); height: 100%; }
221
+ /* Version footer pinned to the bottom of the Studio navigation. */
222
+ .studio-ui .studio-nav-footer { display: flex; align-items: center; gap: 6px; margin-top: auto; padding: 12px 10px 0; color: var(--studio-subtle); font-size: 12px; }
223
+ .studio-ui .studio-icon-button { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: var(--studio-small-radius); background: none; color: var(--studio-info); cursor: pointer; }
224
+ .studio-ui .studio-icon-button:hover:not(:disabled) { background: var(--studio-hover); }
225
+ .studio-ui .studio-icon-button[data-tone=success] { color: var(--studio-success); }
226
+ .studio-ui .studio-icon-button[data-tone=error] { color: var(--studio-error); }
227
+ .studio-ui .studio-icon-button:disabled { cursor: default; opacity: 1; }
228
+ .studio-ui .studio-icon-button[aria-busy=true] { opacity: .5; }
220
229
  .studio-ui nav.studio-nav { display: flex; flex-direction: column; flex-wrap: nowrap; gap: 2px; margin: 0; padding: 24px 12px; border-inline-end: .5px solid var(--studio-line); overflow-y: auto; }
221
230
  .studio-ui .studio-nav-head { display: grid; gap: 2px; padding: 0 10px 8px; }
222
231
  .studio-ui .studio-nav-title { font-size: 14px; }
@@ -607,6 +616,7 @@ var studioStyles = `/* Only this adapter references host tokens. Studio never in
607
616
  .studio-ui nav.studio-nav { flex-direction: row; overflow-x: auto; padding: 8px 12px; border-inline-end: 0; border-bottom: .5px solid var(--studio-line); }
608
617
  .studio-ui .studio-nav-title, .studio-ui .studio-nav-group { display: none; }
609
618
  .studio-ui .studio-nav-item { flex: 0 0 auto; }
619
+ .studio-ui .studio-nav-footer { margin: 0 0 0 auto; padding: 0 0 0 8px; }
610
620
  .studio-ui .studio-scroller { overflow: visible; }
611
621
  }
612
622
  @container studio-main (max-width: 560px) {
@@ -625,6 +635,13 @@ var studioStyles = `/* Only this adapter references host tokens. Studio never in
625
635
  .studio-ui dd { margin-bottom: 8px; }
626
636
  }
627
637
  `;
638
+ var sidebarMarkStyles = `${adapter}
639
+ .studio-ui.studio-sidebar-glyph { display: inline-flex; color: inherit; line-height: 0; }
640
+ .studio-ui .studio-sidebar-dot { fill: var(--studio-info); }
641
+ /* order keeps the mark last even if the sidebar later appends its own label. */
642
+ .studio-ui.studio-sidebar-update { display: inline-flex; flex: none; align-items: center; order: 99; margin-left: auto; color: var(--studio-info); line-height: 0; }
643
+ .studio-ui.studio-sidebar-update[data-phase=failed] { color: var(--studio-error); }
644
+ `;
628
645
 
629
646
  // src/ui.tsx
630
647
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -702,6 +719,9 @@ function SplitButton({ label, onClick, primary = false, disabled = false, action
702
719
  }, children: action.label }, action.label)) })
703
720
  ] });
704
721
  }
722
+ function IconButton({ label, tone = "info", disabled = false, busy = false, onClick, children }) {
723
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "studio-icon-button", "data-tone": tone, title: label, "aria-label": label, "aria-busy": busy || void 0, disabled, onClick, children });
724
+ }
705
725
  function DetailDialog({ open, title, subtitle, onClose, closeLabel, footer, wide = false, compact = false, children }) {
706
726
  const ref = import_react.default.useRef(null);
707
727
  const opener = import_react.default.useRef(null);
@@ -800,7 +820,7 @@ ${item.searchText ?? ""}`.toLowerCase().includes(text)));
800
820
  // src/role-editor.tsx
801
821
  var import_jsx_runtime3 = require("react/jsx-runtime");
802
822
  function RoleEditor({ locale, roles, onChange, loadModels, defaultPersona = "", tools, hiddenTools = [], onHiddenToolsChange, delegates = true }) {
803
- const t = (ko2, en2, zh2) => locale.startsWith("ko") ? ko2 : locale.startsWith("zh") ? zh2 : en2;
823
+ const t = (ko3, en3, zh3) => locale.startsWith("ko") ? ko3 : locale.startsWith("zh") ? zh3 : en3;
804
824
  const [selected, setSelected] = import_react3.default.useState();
805
825
  const [catalog, setCatalog] = import_react3.default.useState();
806
826
  const [error, setError] = import_react3.default.useState("");
@@ -984,7 +1004,7 @@ var import_react9 = __toESM(require("react"), 1);
984
1004
  var import_react4 = __toESM(require("react"), 1);
985
1005
  var import_jsx_runtime4 = require("react/jsx-runtime");
986
1006
  function MasterExtensionsEditor({ locale, actions }) {
987
- const t = (ko2, en2, zh2) => locale.startsWith("ko") ? ko2 : locale.startsWith("zh") ? zh2 : en2;
1007
+ const t = (ko3, en3, zh3) => locale.startsWith("ko") ? ko3 : locale.startsWith("zh") ? zh3 : en3;
988
1008
  const [text, setText] = import_react4.default.useState("[]");
989
1009
  const [error, setError] = import_react4.default.useState("");
990
1010
  const [busy, setBusy] = import_react4.default.useState(false);
@@ -1039,6 +1059,7 @@ var PACKAGE_NAME = "@matamt-dbg/dsh-studio";
1039
1059
  var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
1040
1060
  var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
1041
1061
  var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
1062
+ var STUDIO_VERSION = "0.2.8";
1042
1063
 
1043
1064
  // src/delegation.ts
1044
1065
  var ROLE_TOOL = "subagent_role";
@@ -1467,7 +1488,7 @@ var import_react6 = __toESM(require("react"), 1);
1467
1488
  var import_jsx_runtime6 = require("react/jsx-runtime");
1468
1489
  var message = (error) => error instanceof Error ? error.message : String(error);
1469
1490
  function PresetList({ locale, api, revision, onEdit, onCopy }) {
1470
- const t = (ko2, en2, zh2) => locale.startsWith("ko") ? ko2 : locale.startsWith("zh") ? zh2 : en2;
1491
+ const t = (ko3, en3, zh3) => locale.startsWith("ko") ? ko3 : locale.startsWith("zh") ? zh3 : en3;
1471
1492
  const [load, setLoad] = import_react6.default.useState({ phase: "loading" });
1472
1493
  const [action, setAction] = import_react6.default.useState();
1473
1494
  const [deleting, setDeleting] = import_react6.default.useState();
@@ -1629,7 +1650,7 @@ var import_react7 = __toESM(require("react"), 1);
1629
1650
  var import_jsx_runtime7 = require("react/jsx-runtime");
1630
1651
  var defaultProvidedTools = [];
1631
1652
  function PromptToolsPrototype({ locale, prompt, onPromptChange, provided, onProvidedChange, catalog, catalogError, onPreview, onRequestChange, initialRequest }) {
1632
- const t = (ko2, en2, zh2) => locale.startsWith("ko") ? ko2 : locale.startsWith("zh") ? zh2 : en2;
1653
+ const t = (ko3, en3, zh3) => locale.startsWith("ko") ? ko3 : locale.startsWith("zh") ? zh3 : en3;
1633
1654
  const base = initialRequest?.preset ?? "studio-master";
1634
1655
  const [toolOverrides, setToolOverrides] = import_react7.default.useState(renameLegacyRoleKey(initialRequest?.toolOverrides) ?? {});
1635
1656
  const [sections, setSections] = import_react7.default.useState(initialRequest?.sections ?? {});
@@ -1841,7 +1862,7 @@ function parameterFields(schema, prefix = "") {
1841
1862
  // src/detail-layout.tsx
1842
1863
  var import_react8 = __toESM(require("react"), 1);
1843
1864
  var import_jsx_runtime8 = require("react/jsx-runtime");
1844
- function DetailLayout({ title, subtitle, navigation, pages, selected, onSelect }) {
1865
+ function DetailLayout({ title, subtitle, navigation, pages, selected, onSelect, footer }) {
1845
1866
  const id = import_react8.default.useId();
1846
1867
  const scroller = import_react8.default.useRef(null);
1847
1868
  import_react8.default.useLayoutEffect(() => {
@@ -1866,7 +1887,8 @@ function DetailLayout({ title, subtitle, navigation, pages, selected, onSelect }
1866
1887
  children: page.label
1867
1888
  }
1868
1889
  )
1869
- ] }, page.id))
1890
+ ] }, page.id)),
1891
+ footer
1870
1892
  ] }),
1871
1893
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "studio-scroller", ref: scroller, "data-studio-scroller": "", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "studio-content", children: pages.map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1872
1894
  "section",
@@ -1892,8 +1914,8 @@ var seeds = [
1892
1914
  { name: "Researcher", role: "research", model: "inherit", reasoning: "inherit", spawn: "fresh", persona: "Investigate primary sources and cite the evidence." },
1893
1915
  { name: "Reviewer", role: "review", model: "inherit", reasoning: "inherit", spawn: "fork", persona: "Review correctness and identify concrete risks." }
1894
1916
  ];
1895
- function OptimizationPrototype({ locale, catalog, catalogError, onPreview, presetActions, extensionActions }) {
1896
- const t = (ko2, en2, zh2) => locale.startsWith("ko") ? ko2 : locale.startsWith("zh") ? zh2 : en2;
1917
+ function OptimizationPrototype({ locale, catalog, catalogError, onPreview, presetActions, extensionActions, navFooter }) {
1918
+ const t = (ko3, en3, zh3) => locale.startsWith("ko") ? ko3 : locale.startsWith("zh") ? zh3 : en3;
1897
1919
  const [page, setPage] = import_react9.default.useState("list");
1898
1920
  const [common, setCommon] = import_react9.default.useState({ ...initial });
1899
1921
  const [agents, setAgents] = import_react9.default.useState(() => seeds.map((a) => ({ ...a })));
@@ -1936,9 +1958,9 @@ function OptimizationPrototype({ locale, catalog, catalogError, onPreview, prese
1936
1958
  setSaving(false);
1937
1959
  }
1938
1960
  };
1939
- const editPreset = (item, copy2 = false) => {
1940
- setEditing(copy2 ? { ...item, id: "" } : item);
1941
- setName(copy2 ? `${item.name} Copy` : item.name);
1961
+ const editPreset = (item, copy3 = false) => {
1962
+ setEditing(copy3 ? { ...item, id: "" } : item);
1963
+ setName(copy3 ? `${item.name} Copy` : item.name);
1942
1964
  setDescription(item.description);
1943
1965
  setCommon({ ...initial, prompt: item.request.prompt, spill: item.conveniences?.spill ?? initial.spill });
1944
1966
  setProvided([...item.request.tools]);
@@ -1965,7 +1987,7 @@ function OptimizationPrototype({ locale, catalog, catalogError, onPreview, prese
1965
1987
  { id: "agents", label: t("\uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8", "Subagent settings", "\u5B50\u4EE3\u7406\u8BBE\u7F6E"), content: agentPage }
1966
1988
  ];
1967
1989
  const tabId = import_react9.default.useId();
1968
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DetailLayout, { title: "DSH Studio", subtitle: t("\uC5D0\uC774\uC804\uD2B8 \uD504\uB9AC\uC14B \uD3B8\uC9D1\uAE30", "Agent preset editor", "\u4EE3\u7406\u9884\u8BBE\u7F16\u8F91\u5668"), navigation: t("Studio \uBA54\uB274", "Studio menu", "\u9884\u8BBE\u5BFC\u822A"), selected: page, onSelect: (next) => {
1990
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DetailLayout, { footer: navFooter, title: "DSH Studio", subtitle: t("\uC5D0\uC774\uC804\uD2B8 \uD504\uB9AC\uC14B \uD3B8\uC9D1\uAE30", "Agent preset editor", "\u4EE3\u7406\u9884\u8BBE\u7F16\u8F91\u5668"), navigation: t("Studio \uBA54\uB274", "Studio menu", "\u9884\u8BBE\u5BFC\u822A"), selected: page, onSelect: (next) => {
1969
1991
  if (next === "create" && editing) {
1970
1992
  setEditing(void 0);
1971
1993
  setName("");
@@ -2006,7 +2028,7 @@ function OptimizationPrototype({ locale, catalog, catalogError, onPreview, prese
2006
2028
  }
2007
2029
 
2008
2030
  // src/shell.tsx
2009
- var import_react11 = __toESM(require("react"), 1);
2031
+ var import_react12 = __toESM(require("react"), 1);
2010
2032
  var import_dsh_client_ui_slots = require("@deepseek-ai/dsh-client-ui-slots");
2011
2033
 
2012
2034
  // src/service-layout.tsx
@@ -2050,8 +2072,145 @@ function ServiceLayout({ locale, services, renderSlot }) {
2050
2072
  ] });
2051
2073
  }
2052
2074
 
2053
- // src/shell.tsx
2075
+ // src/update-store.ts
2076
+ function createUpdateStore(actions, current) {
2077
+ let state = { phase: "idle", current };
2078
+ const listeners = /* @__PURE__ */ new Set();
2079
+ const set = (next) => {
2080
+ state = next;
2081
+ listeners.forEach((listener) => listener());
2082
+ };
2083
+ return {
2084
+ getSnapshot: () => state,
2085
+ subscribe(listener) {
2086
+ listeners.add(listener);
2087
+ return () => {
2088
+ listeners.delete(listener);
2089
+ };
2090
+ },
2091
+ async refresh(force = false) {
2092
+ if (state.phase === "updating" || state.phase === "installed") return;
2093
+ try {
2094
+ const status = await actions.status(force);
2095
+ const phase = () => state.phase;
2096
+ if (phase() === "updating" || phase() === "installed") return;
2097
+ if (status.installed) {
2098
+ set({ phase: "installed", current: status.current, version: status.installed });
2099
+ return;
2100
+ }
2101
+ const version = status.available && !status.blocked ? status.available : void 0;
2102
+ set({ phase: "idle", current: status.current, ...version ? { version } : {} });
2103
+ } catch {
2104
+ }
2105
+ },
2106
+ async update() {
2107
+ if (state.phase !== "idle" && state.phase !== "failed") return;
2108
+ const version = state.version;
2109
+ if (!version) return;
2110
+ set({ phase: "updating", current: state.current, version });
2111
+ try {
2112
+ const result = await actions.update(version);
2113
+ if (result.status === "restart-required" || result.status === "applied") set({ phase: "installed", current: state.current, version: result.version });
2114
+ else set({ phase: "failed", current: state.current, version, message: result.message ?? result.status });
2115
+ } catch (error) {
2116
+ set({ phase: "failed", current: state.current, version, message: error instanceof Error ? error.message : String(error) });
2117
+ }
2118
+ }
2119
+ };
2120
+ }
2121
+
2122
+ // src/update-indicator.tsx
2123
+ var import_react11 = __toESM(require("react"), 1);
2124
+ var import_react_dom = require("react-dom");
2054
2125
  var import_jsx_runtime11 = require("react/jsx-runtime");
2126
+ var ko2 = {
2127
+ available: (v) => `v${v}\uB85C \uC5C5\uB370\uC774\uD2B8`,
2128
+ updating: (v) => `v${v} \uC124\uCE58 \uC911`,
2129
+ installed: (v) => `v${v}\uB97C \uC124\uCE58\uD588\uC2B5\uB2C8\uB2E4. DSH\uB97C \uB2E4\uC2DC \uC2DC\uC791\uD558\uBA74 \uC801\uC6A9\uB429\uB2C8\uB2E4.`,
2130
+ failed: (v, message2) => `v${v}\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uB824\uBA74 \uB204\uB974\uC138\uC694. (${message2})`
2131
+ };
2132
+ var en2 = {
2133
+ available: (v) => `Update to v${v}`,
2134
+ updating: (v) => `Installing v${v}`,
2135
+ installed: (v) => `Installed v${v}. Restart DSH to use it.`,
2136
+ failed: (v, message2) => `Could not update to v${v}. Select to try again. (${message2})`
2137
+ };
2138
+ var zh2 = {
2139
+ available: (v) => `\u66F4\u65B0\u5230 v${v}`,
2140
+ updating: (v) => `\u6B63\u5728\u5B89\u88C5 v${v}`,
2141
+ installed: (v) => `\u5DF2\u5B89\u88C5 v${v}\u3002\u91CD\u542F DSH \u540E\u751F\u6548`,
2142
+ failed: (v, message2) => `\u65E0\u6CD5\u66F4\u65B0\u5230 v${v}\u3002\u70B9\u51FB\u91CD\u8BD5\uFF08${message2}\uFF09`
2143
+ };
2144
+ var copy2 = (locale) => locale.startsWith("ko") ? ko2 : locale.startsWith("zh") ? zh2 : en2;
2145
+ function useUpdateState(store) {
2146
+ return import_react11.default.useSyncExternalStore(store.subscribe, store.getSnapshot);
2147
+ }
2148
+ function updateLabel(state, locale) {
2149
+ const t = copy2(locale);
2150
+ if (state.phase === "updating") return t.updating(state.version);
2151
+ if (state.phase === "installed") return t.installed(state.version);
2152
+ if (state.phase === "failed") return t.failed(state.version, state.message);
2153
+ return state.version ? t.available(state.version) : void 0;
2154
+ }
2155
+ function UpdateGlyph({ phase, size = 14 }) {
2156
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2157
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: "8", cy: "8", r: "6.5" }),
2158
+ phase === "installed" ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M5.2 8.2l1.9 1.9 3.7-4" }) : phase === "failed" ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M8 4.8v3.8M8 11.2v.01" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M8 11V5.2M5.4 7.6L8 5l2.6 2.6" })
2159
+ ] });
2160
+ }
2161
+ function VersionFooter({ store, locale }) {
2162
+ const state = useUpdateState(store);
2163
+ const label = updateLabel(state, locale);
2164
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "studio-nav-footer", children: [
2165
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "studio-mono", children: [
2166
+ "v",
2167
+ state.current
2168
+ ] }),
2169
+ label && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2170
+ IconButton,
2171
+ {
2172
+ label,
2173
+ tone: state.phase === "failed" ? "error" : state.phase === "installed" ? "success" : "info",
2174
+ disabled: state.phase === "updating" || state.phase === "installed",
2175
+ busy: state.phase === "updating",
2176
+ onClick: () => {
2177
+ void store.update();
2178
+ },
2179
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(UpdateGlyph, { phase: state.phase })
2180
+ }
2181
+ )
2182
+ ] });
2183
+ }
2184
+ function SidebarIcon({ size = 20, store, locale }) {
2185
+ const state = useUpdateState(store);
2186
+ const label = updateLabel(state, locale);
2187
+ const glyph = import_react11.default.useRef(null);
2188
+ const [row, setRow] = import_react11.default.useState(null);
2189
+ import_react11.default.useLayoutEffect(() => {
2190
+ setRow(glyph.current?.closest("button") ?? null);
2191
+ }, [size]);
2192
+ const wide = size <= 16;
2193
+ const marked = label !== void 0 && state.phase !== "installed";
2194
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
2195
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "studio-ui studio-sidebar-glyph", children: [
2196
+ marked && !wide && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: sidebarMarkStyles }),
2197
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { ref: glyph, width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", "aria-hidden": "true", children: [
2198
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "3", width: "7", height: "7", rx: "2" }),
2199
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "14", y: "3", width: "7", height: "7", rx: "2" }),
2200
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "14", width: "7", height: "7", rx: "2" }),
2201
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M17.5 14v7M14 17.5h7" }),
2202
+ marked && !wide && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { className: "studio-sidebar-dot", cx: "21", cy: "3", r: "3", stroke: "none" })
2203
+ ] })
2204
+ ] }),
2205
+ marked && wide && row && (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "studio-ui studio-sidebar-update", "data-phase": state.phase, title: label, role: "img", "aria-label": label, children: [
2206
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: sidebarMarkStyles }),
2207
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(UpdateGlyph, { phase: state.phase, size: 14 })
2208
+ ] }), row)
2209
+ ] });
2210
+ }
2211
+
2212
+ // src/shell.tsx
2213
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2055
2214
  var inject = ["slots", "locale", "remote"];
2056
2215
  var panel = "local-dsh-studio";
2057
2216
  async function apply(ctx) {
@@ -2072,7 +2231,18 @@ async function apply(ctx) {
2072
2231
  invocation: { kind: "direct" },
2073
2232
  parameters: [{ name: "request", wire: "request", source: "json", codec: { mode: "strict", typeSymbol: "PreviewRequest", create: () => ({ parse: parsePreviewRequest }) } }],
2074
2233
  result: { mode: "src-json" }
2075
- }, ...["listPresets", "savePreset", "applyPreset", "unapplyPreset", "roleModels", "deletePreset", "readExtensions", "saveExtensions"].map((method) => ({
2234
+ }, ...["updateStatus", "updateStudio"].map((method) => ({
2235
+ id: `${PACKAGE_NAME}#studioCatalog/${method}`,
2236
+ service: "studioCatalog",
2237
+ namespace: "studioCatalog",
2238
+ method,
2239
+ invocation: { kind: "direct" },
2240
+ parameters: [{ name: method === "updateStatus" ? "force" : "version", wire: method === "updateStatus" ? "force" : "version", source: "json", codec: { mode: "strict", typeSymbol: "string", create: () => ({ parse: (value) => {
2241
+ if (typeof value !== "string") throw new TypeError("Expected a string");
2242
+ return value;
2243
+ } }) } }],
2244
+ result: { mode: "src-json" }
2245
+ })), ...["listPresets", "savePreset", "applyPreset", "unapplyPreset", "roleModels", "deletePreset", "readExtensions", "saveExtensions"].map((method) => ({
2076
2246
  id: `${PACKAGE_NAME}#studioCatalog/${method}`,
2077
2247
  service: "studioCatalog",
2078
2248
  namespace: "studioCatalog",
@@ -2096,35 +2266,35 @@ async function apply(ctx) {
2096
2266
  };
2097
2267
  ctx.effect(() => ctx.slots.subscribe("local.dsh-studio.services", syncServices));
2098
2268
  ctx.effect(() => ctx.locale.subscribe(syncServices));
2269
+ const updateActions = /* @__PURE__ */ (() => {
2270
+ const call = async (method, arg) => {
2271
+ const service = ctx.get("remote.studioCatalog");
2272
+ const fn = service?.[method];
2273
+ if (!fn) throw new Error("Studio update API unavailable");
2274
+ const result = await fn(arg);
2275
+ if (!result.ok) throw new Error(result.error?.message ?? "Update request failed");
2276
+ return result.value;
2277
+ };
2278
+ return {
2279
+ status: (force) => call("updateStatus", force ? "force" : ""),
2280
+ update: (version) => call("updateStudio", version)
2281
+ };
2282
+ })();
2283
+ const updates = createUpdateStore(updateActions, STUDIO_VERSION);
2284
+ void updates.refresh();
2099
2285
  function StudioServices({ renderSlot }) {
2100
- const items = import_react11.default.useSyncExternalStore((listener) => {
2286
+ const items = import_react12.default.useSyncExternalStore((listener) => {
2101
2287
  serviceListeners.add(listener);
2102
2288
  return () => {
2103
2289
  serviceListeners.delete(listener);
2104
2290
  };
2105
2291
  }, () => services);
2106
- const locale = import_react11.default.useSyncExternalStore((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getLocale().active);
2107
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StudioSurface, { className: "studio-shell", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("main", { className: "studio-page", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ServiceLayout, { locale, services: items, renderSlot }) }) });
2292
+ const locale = import_react12.default.useSyncExternalStore((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getLocale().active);
2293
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StudioSurface, { className: "studio-shell", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("main", { className: "studio-page", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ServiceLayout, { locale, services: items, renderSlot }) }) });
2108
2294
  }
2109
2295
  function Icon({ size = 20 }) {
2110
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2111
- "svg",
2112
- {
2113
- width: size,
2114
- height: size,
2115
- viewBox: "0 0 24 24",
2116
- fill: "none",
2117
- stroke: "currentColor",
2118
- strokeWidth: "1.7",
2119
- "aria-hidden": "true",
2120
- children: [
2121
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "3", width: "7", height: "7", rx: "2" }),
2122
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "14", y: "3", width: "7", height: "7", rx: "2" }),
2123
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "14", width: "7", height: "7", rx: "2" }),
2124
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M17.5 14v7M14 17.5h7" })
2125
- ]
2126
- }
2127
- );
2296
+ const locale = import_react12.default.useSyncExternalStore((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getLocale().active);
2297
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SidebarIcon, { size, store: updates, locale });
2128
2298
  }
2129
2299
  ctx.slots.inject(
2130
2300
  "main",
@@ -2140,10 +2310,10 @@ async function apply(ctx) {
2140
2310
  )
2141
2311
  );
2142
2312
  function OptimizationPage() {
2143
- const locale = import_react11.default.useSyncExternalStore((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getLocale().active);
2144
- const [catalog, setCatalog] = import_react11.default.useState();
2145
- const [catalogError, setCatalogError] = import_react11.default.useState("");
2146
- import_react11.default.useEffect(() => {
2313
+ const locale = import_react12.default.useSyncExternalStore((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getLocale().active);
2314
+ const [catalog, setCatalog] = import_react12.default.useState();
2315
+ const [catalogError, setCatalogError] = import_react12.default.useState("");
2316
+ import_react12.default.useEffect(() => {
2147
2317
  let active = true;
2148
2318
  const remote2 = ctx.get("remote.studioCatalog");
2149
2319
  if (!remote2) {
@@ -2161,7 +2331,7 @@ async function apply(ctx) {
2161
2331
  active = false;
2162
2332
  };
2163
2333
  }, []);
2164
- const presetActions = import_react11.default.useMemo(() => {
2334
+ const presetActions = import_react12.default.useMemo(() => {
2165
2335
  const call = async (method, ...args) => {
2166
2336
  const service = ctx.get("remote.studioCatalog");
2167
2337
  const result = await service[method](...args);
@@ -2177,7 +2347,7 @@ async function apply(ctx) {
2177
2347
  delete: (id) => call("deletePreset", id)
2178
2348
  };
2179
2349
  }, []);
2180
- const extensionActions = import_react11.default.useMemo(() => {
2350
+ const extensionActions = import_react12.default.useMemo(() => {
2181
2351
  const call = async (method, ...args) => {
2182
2352
  const service = ctx.get("remote.studioCatalog");
2183
2353
  const result = await service[method](...args);
@@ -2188,7 +2358,10 @@ async function apply(ctx) {
2188
2358
  setCatalog(await call("saveExtensions", payload));
2189
2359
  } };
2190
2360
  }, []);
2191
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(OptimizationPrototype, { extensionActions, presetActions, locale, catalog, catalogError, onPreview: async (request) => {
2361
+ import_react12.default.useEffect(() => {
2362
+ void updates.refresh();
2363
+ }, []);
2364
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OptimizationPrototype, { navFooter: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(VersionFooter, { store: updates, locale }), extensionActions, presetActions, locale, catalog, catalogError, onPreview: async (request) => {
2192
2365
  const service = ctx.get("remote.studioCatalog");
2193
2366
  const response = await service.preview(request);
2194
2367
  if (!response.ok) throw new Error(response.error?.message ?? "Preview failed");
package/index.js CHANGED
@@ -56,6 +56,7 @@ var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
56
56
  function currentPluginName(name) {
57
57
  return name === LEGACY_PACKAGE_NAME || name.startsWith(`${LEGACY_PACKAGE_NAME}/`) ? PACKAGE_NAME + name.slice(LEGACY_PACKAGE_NAME.length) : name;
58
58
  }
59
+ var STUDIO_VERSION = "0.2.8";
59
60
 
60
61
  // src/delegation.ts
61
62
  var ROLE_TOOL = "subagent_role";
@@ -383,7 +384,7 @@ var MasterExtensions = class {
383
384
  };
384
385
 
385
386
  // src/host/shell.ts
386
- import { dirname as dirname3, join as join2 } from "node:path";
387
+ import { dirname as dirname3, join as join3 } from "node:path";
387
388
  import { randomUUID as randomUUID4 } from "node:crypto";
388
389
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
389
390
 
@@ -1170,9 +1171,142 @@ var PresetStore = class {
1170
1171
  }
1171
1172
  };
1172
1173
 
1174
+ // src/host/studio-update.ts
1175
+ import { readFile as readFile3 } from "node:fs/promises";
1176
+ import { join as join2 } from "node:path";
1177
+ import { evaluatePluginCompatibility, getDshRuntimeVersion } from "@deepseek-ai/dsh-app-boot";
1178
+
1179
+ // src/update-check.ts
1180
+ var STABLE = /^(\d+)\.(\d+)\.(\d+)$/;
1181
+ function compareStable(a, b) {
1182
+ const x = STABLE.exec(a);
1183
+ const y = STABLE.exec(b);
1184
+ if (!x || !y) return Number(Boolean(x)) - Number(Boolean(y));
1185
+ for (let i = 1; i <= 3; i++) {
1186
+ const diff = Number(x[i]) - Number(y[i]);
1187
+ if (diff) return Math.sign(diff);
1188
+ }
1189
+ return 0;
1190
+ }
1191
+ function isNewer(candidate, current) {
1192
+ return STABLE.test(candidate) && compareStable(candidate, current.split("-")[0]) > 0;
1193
+ }
1194
+ function pickUpdate(document, current, compatible) {
1195
+ const latest = document["dist-tags"]?.latest;
1196
+ const candidates = Object.entries(document.versions ?? {}).filter(([version, manifest]) => isNewer(version, current) && !manifest.deprecated).sort(([a], [b]) => compareStable(b, a));
1197
+ const available = candidates.find(([, manifest]) => compatible(manifest))?.[0];
1198
+ const incompatibleLatest = latest !== void 0 && isNewer(latest, current) && latest !== available && candidates.some(([version]) => version === latest) ? latest : void 0;
1199
+ return { ...latest ? { latest } : {}, ...available ? { available } : {}, ...incompatibleLatest ? { incompatibleLatest } : {} };
1200
+ }
1201
+ function packumentUrl(registry, name) {
1202
+ return `${registry.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
1203
+ }
1204
+
1205
+ // src/host/studio-update.ts
1206
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1207
+ var CACHE_MS = 10 * 6e4;
1208
+ var StudioUpdater = class {
1209
+ constructor(ctx, fetcher = globalThis.fetch) {
1210
+ this.ctx = ctx;
1211
+ this.fetcher = fetcher;
1212
+ }
1213
+ cached;
1214
+ installing;
1215
+ manager() {
1216
+ return this.ctx.get("pluginManager");
1217
+ }
1218
+ /** Read the current status, reusing a recent registry answer unless `force`. */
1219
+ status(force = false) {
1220
+ if (!force && this.cached && Date.now() - this.cached.at < CACHE_MS) return this.cached.status;
1221
+ const status = this.check();
1222
+ this.cached = { at: Date.now(), status };
1223
+ void status.catch(() => {
1224
+ if (this.cached?.status === status) this.cached = void 0;
1225
+ });
1226
+ return status;
1227
+ }
1228
+ async check() {
1229
+ const result = { current: STUDIO_VERSION };
1230
+ let runtimeVersion;
1231
+ try {
1232
+ runtimeVersion = getDshRuntimeVersion();
1233
+ result.runtimeVersion = runtimeVersion;
1234
+ } catch {
1235
+ }
1236
+ const { source, installed } = await this.profilePackage();
1237
+ if (source) result.source = source;
1238
+ if (installed && installed !== STUDIO_VERSION) result.installed = installed;
1239
+ const baseline = installed && isNewer(installed, STUDIO_VERSION) ? installed : STUDIO_VERSION;
1240
+ const manager = this.manager();
1241
+ if (!manager) result.blocked = "no-plugin-manager";
1242
+ else if (!source) result.blocked = "not-profile-dependency";
1243
+ try {
1244
+ const registries = manager ? await manager.registries().catch(() => void 0) : void 0;
1245
+ const registry = registries?.registry ?? registries?.resolved ?? DEFAULT_REGISTRY;
1246
+ const response = await this.fetcher(packumentUrl(registry, PACKAGE_NAME), {
1247
+ headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" },
1248
+ signal: AbortSignal.timeout(15e3)
1249
+ });
1250
+ if (!response.ok) throw new Error(`Registry answered ${response.status}`);
1251
+ const document = await response.json();
1252
+ Object.assign(result, pickUpdate(document, baseline, (manifest) => {
1253
+ try {
1254
+ return evaluatePluginCompatibility({ name: PACKAGE_NAME, version: "0.0.0", ...manifest }, {}, runtimeVersion) === void 0;
1255
+ } catch {
1256
+ return false;
1257
+ }
1258
+ }));
1259
+ } catch (error) {
1260
+ result.error = error instanceof Error ? error.message : String(error);
1261
+ }
1262
+ return result;
1263
+ }
1264
+ /** How the profile declares Studio and which version its files hold now. */
1265
+ async profilePackage() {
1266
+ const dir = this.ctx.get("profileContext")?.dir ?? process.env.DSH_PROFILE_DIR;
1267
+ if (!dir) return {};
1268
+ const json2 = async (path) => {
1269
+ try {
1270
+ return JSON.parse(await readFile3(path, "utf8"));
1271
+ } catch {
1272
+ return void 0;
1273
+ }
1274
+ };
1275
+ const manifest = await json2(join2(dir, "package.json"));
1276
+ const source = manifest?.dependencies?.[PACKAGE_NAME];
1277
+ const installed = (await json2(join2(dir, "node_modules", ...PACKAGE_NAME.split("/"), "package.json")))?.version;
1278
+ return { ...source ? { source } : {}, ...typeof installed === "string" ? { installed } : {} };
1279
+ }
1280
+ /**
1281
+ * Install one exact version offered by {@link status}. The running Studio keeps its code until DSH restarts.
1282
+ * @param version Exact version; must equal the current offer so arbitrary specs never reach pnpm.
1283
+ */
1284
+ update(version) {
1285
+ if (this.installing) return this.installing;
1286
+ const run = (async () => {
1287
+ const status = await this.status();
1288
+ if (status.available !== version) throw new Error(`Version ${version} is not the offered update`);
1289
+ if (status.blocked) throw new Error(`Studio cannot update this install (${status.blocked})`);
1290
+ const manager = this.manager();
1291
+ const result = await manager.installBundle(`${PACKAGE_NAME}@${version}`, { enabled: true });
1292
+ const installed = result.version ?? version;
1293
+ if (result.application === "failed" || result.application === "cancelled") {
1294
+ const detail = result.error?.diagnostic ?? result.error?.code ?? result.packageResult?.output?.trim().split("\n").slice(-3).join("\n");
1295
+ return { status: result.application, version: installed, ...detail ? { message: detail } : {} };
1296
+ }
1297
+ this.cached = void 0;
1298
+ return { status: result.application === "applied" ? "applied" : "restart-required", version: installed };
1299
+ })();
1300
+ this.installing = run;
1301
+ return run.finally(() => {
1302
+ this.installing = void 0;
1303
+ });
1304
+ }
1305
+ };
1306
+
1173
1307
  // src/host/shell.ts
1174
- var _preview_dec, _saveExtensions_dec, _readExtensions_dec, _read_dec, _deletePreset_dec, _roleModels_dec, _unapplyPreset_dec, _applyPreset_dec, _savePreset_dec, _listPresets_dec, _a, _init;
1175
- var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec = [Remote], _savePreset_dec = [Remote], _applyPreset_dec = [Remote], _unapplyPreset_dec = [Remote], _roleModels_dec = [Remote], _deletePreset_dec = [Remote], _read_dec = [Remote], _readExtensions_dec = [Remote], _saveExtensions_dec = [Remote], _preview_dec = [Remote], _a) {
1308
+ var _preview_dec, _saveExtensions_dec, _readExtensions_dec, _read_dec, _deletePreset_dec, _updateStudio_dec, _updateStatus_dec, _roleModels_dec, _unapplyPreset_dec, _applyPreset_dec, _savePreset_dec, _listPresets_dec, _a, _init;
1309
+ var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec = [Remote], _savePreset_dec = [Remote], _applyPreset_dec = [Remote], _unapplyPreset_dec = [Remote], _roleModels_dec = [Remote], _updateStatus_dec = [Remote], _updateStudio_dec = [Remote], _deletePreset_dec = [Remote], _read_dec = [Remote], _readExtensions_dec = [Remote], _saveExtensions_dec = [Remote], _preview_dec = [Remote], _a) {
1176
1310
  constructor(ctx) {
1177
1311
  super(ctx, "studioCatalog");
1178
1312
  __runInitializers(_init, 5, this);
@@ -1182,8 +1316,9 @@ var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec =
1182
1316
  __publicField(this, "stopped", false);
1183
1317
  __publicField(this, "extensions");
1184
1318
  __publicField(this, "changes", Promise.resolve());
1319
+ __publicField(this, "updater", new StudioUpdater(this.ctx));
1185
1320
  const storePath = studioStorePath(process.env, ctx.get("profileContext"));
1186
- this.extensions = new MasterExtensions(join2(dirname3(storePath), "master-extensions.json"));
1321
+ this.extensions = new MasterExtensions(join3(dirname3(storePath), "master-extensions.json"));
1187
1322
  this.store = new PresetStore(ctx, storePath);
1188
1323
  this.snapshot = new Promise((resolve2) => {
1189
1324
  setImmediate(resolve2);
@@ -1219,6 +1354,12 @@ var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec =
1219
1354
  if (!service) throw new Error("Model catalog API unavailable");
1220
1355
  return service.modelCatalog();
1221
1356
  }
1357
+ async updateStatus(force) {
1358
+ return this.updater.status(force === "force");
1359
+ }
1360
+ async updateStudio(version) {
1361
+ return this.updater.update(version);
1362
+ }
1222
1363
  async deletePreset(id) {
1223
1364
  return this.store.delete(id);
1224
1365
  }
@@ -1319,6 +1460,8 @@ __decorateElement(_init, 1, "savePreset", _savePreset_dec, StudioCatalog);
1319
1460
  __decorateElement(_init, 1, "applyPreset", _applyPreset_dec, StudioCatalog);
1320
1461
  __decorateElement(_init, 1, "unapplyPreset", _unapplyPreset_dec, StudioCatalog);
1321
1462
  __decorateElement(_init, 1, "roleModels", _roleModels_dec, StudioCatalog);
1463
+ __decorateElement(_init, 1, "updateStatus", _updateStatus_dec, StudioCatalog);
1464
+ __decorateElement(_init, 1, "updateStudio", _updateStudio_dec, StudioCatalog);
1322
1465
  __decorateElement(_init, 1, "deletePreset", _deletePreset_dec, StudioCatalog);
1323
1466
  __decorateElement(_init, 1, "read", _read_dec, StudioCatalog);
1324
1467
  __decorateElement(_init, 1, "readExtensions", _readExtensions_dec, StudioCatalog);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matamt-dbg/dsh-studio",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "DSH Studio: build, edit and apply DeepSeek Harness agent presets — tools, prompts, Configs and role-based subagents — from the web GUI.",
5
5
  "keywords": [
6
6
  "deepseek-harness",