@klarkxy/dsh-safe-auto 0.3.0 → 0.5.0

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.
package/lib/client.js CHANGED
@@ -294,7 +294,14 @@ function useModelCatalog(load, options = {}) {
294
294
  refresh
295
295
  };
296
296
  }
297
- /** Also accepts a mapped state from a caller that owns broader async orchestration. */
297
+ /**
298
+ * Also accepts a mapped state from a caller that owns broader async orchestration.
299
+ *
300
+ * The dsh-ui-* classes carry the styling and resolve through the consuming
301
+ * plugin's own officialUiCss scope (every consumer mounts its root class, so
302
+ * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
303
+ * stays as a structural hook for tests and layout, never a colour source.
304
+ */
298
305
  function ModelCatalogStatus({ state, locale }) {
299
306
  const zh = locale?.toLowerCase().startsWith("zh") === true;
300
307
  const copy = zh ? {
@@ -310,15 +317,17 @@ function ModelCatalogStatus({ state, locale }) {
310
317
  };
311
318
  if (!state.loading && !state.error && state.failures.length === 0) return null;
312
319
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
313
- className: "dsh-model-catalog-status",
320
+ className: "dsh-model-catalog-status dsh-ui-stack",
314
321
  lang: zh ? "zh" : "en",
315
322
  children: [
316
323
  state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
317
324
  role: "status",
325
+ className: "dsh-ui-loading",
318
326
  children: copy.loading
319
327
  }) : null,
320
328
  state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
321
329
  role: "alert",
330
+ className: "dsh-ui-error dsh-ui-wrap",
322
331
  children: [
323
332
  copy.error,
324
333
  ": ",
@@ -327,20 +336,27 @@ function ModelCatalogStatus({ state, locale }) {
327
336
  }) : null,
328
337
  state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
329
338
  role: "status",
330
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
331
- failure.name || failure.id,
332
- ": ",
333
- failure.message
334
- ] }, `${failure.id}:${index}`)) })]
339
+ className: "dsh-ui-meta",
340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
341
+ className: "dsh-ui-list",
342
+ children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
343
+ failure.name || failure.id,
344
+ ": ",
345
+ failure.message
346
+ ] }, `${failure.id}:${index}`))
347
+ })]
335
348
  }) : null,
336
- state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
337
- type: "button",
338
- size: "sm",
339
- disabled: state.loading,
340
- onClick: () => {
341
- state.refresh();
342
- },
343
- children: copy.retry
349
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
350
+ className: "dsh-ui-actions",
351
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
352
+ type: "button",
353
+ size: "sm",
354
+ disabled: state.loading,
355
+ onClick: () => {
356
+ state.refresh();
357
+ },
358
+ children: copy.retry
359
+ })
344
360
  }) : null
345
361
  ]
346
362
  });
@@ -358,7 +374,8 @@ function ModelCatalogStatus({ state, locale }) {
358
374
  * Sticky group headings, subsequence ranking, highlight walking and focus
359
375
  * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
360
376
  * ModelSelect.module.css changes, this copy must be re-transcribed; styles
361
- * below may only reference --dsw-* theme tokens, never literal colors.
377
+ * below may only reference allowlisted theme tokens (the --dsw-* set plus the
378
+ * host's --ds-* motion tokens), never literal colors.
362
379
  *
363
380
  * It statically imports the primitives, so only browser clients should load
364
381
  * this entry; host code and node tests use the core entry (`./index.ts`).
@@ -866,7 +883,8 @@ function ModelMenu(props) {
866
883
  * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
867
884
  * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
868
885
  * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
869
- * Only --dsw-* theme tokens may appear here — a literal color is a bug.
886
+ * Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
887
+ * color is a bug.
870
888
  */
871
889
  const modelMenuCss = `
872
890
  .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
@@ -889,12 +907,14 @@ const modelMenuCss = `
889
907
  display: flex;
890
908
  }
891
909
  .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
892
- .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
910
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
893
911
  .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
894
912
  .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
895
913
  .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
896
- .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
914
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
897
915
  .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
916
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
917
+ * native menu layer's 1100 verbatim. */
898
918
  .dsh-model-menu-menu {
899
919
  z-index: 1100;
900
920
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
@@ -920,6 +940,8 @@ const modelMenuCss = `
920
940
  .dsh-model-menu-searchWithQuery { padding-right: 34px; }
921
941
  .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
922
942
  .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
943
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
944
+ * radius token scale does not describe. */
923
945
  .dsh-model-menu-searchClear {
924
946
  width: 24px;
925
947
  height: 24px;
@@ -1167,6 +1189,17 @@ ${scope} {
1167
1189
  /* --- Layout scaffolding. --- */
1168
1190
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
1169
1191
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
1192
+ /* Plugin detail pages share one reading column. These are layout recipes;
1193
+ * controls keep their own primitive styles. Form spacing follows the host's
1194
+ * settings-form/fields.module.css and SettingsForm.module.css. */
1195
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
1196
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
1197
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
1198
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
1199
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
1200
+ @media (max-width: 560px) {
1201
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
1202
+ }
1170
1203
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
1171
1204
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1172
1205
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -1194,6 +1227,9 @@ ${scope} {
1194
1227
  * official primitive, so this only owns the vertical rhythm. --- */
1195
1228
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1196
1229
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
1230
+ /* Grid fields share a row, so the stacked-field divider must not win later in
1231
+ * the cascade. Reset it here, after the field recipe. */
1232
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
1197
1233
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1198
1234
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
1199
1235
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -1384,7 +1420,7 @@ ${scope} {
1384
1420
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
1385
1421
  }
1386
1422
  }
1387
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
1423
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
1388
1424
  }
1389
1425
  //#endregion
1390
1426
  //#region ../dsh-plugin-kit/lib/client-utils.js
@@ -1414,25 +1450,18 @@ function createPluginReadGate(now = Date.now) {
1414
1450
  }
1415
1451
  //#endregion
1416
1452
  //#region ../dsh-plugin-kit/lib/contracts.js
1417
- /**
1418
- * Remote contribution for model-settings change notifications.
1419
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
1420
- * subscribers degrade silently when that package is not installed.
1421
- * The stream carries invalidations only — `ready` is never a settings snapshot.
1422
- */
1423
- const MODEL_SETTINGS_CHANGES_REMOTE = {
1424
- package: "@klarkxy/dsh-model-hub",
1425
- descriptors: [{
1426
- id: "modelSettingsChanges/watch",
1427
- service: "modelSettingsChangesController",
1428
- namespace: "modelSettingsChanges",
1429
- method: "watch",
1430
- mode: "stream",
1431
- invocation: { kind: "direct" },
1432
- parameters: [],
1433
- cancellation: { parameter: "signal" }
1434
- }]
1435
- };
1453
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1454
+ function modelSettingsChangesIdentity(packageName) {
1455
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1456
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1457
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1458
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1459
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1460
+ return {
1461
+ service: segment,
1462
+ namespace: segment
1463
+ };
1464
+ }
1436
1465
  //#endregion
1437
1466
  //#region ../dsh-plugin-kit/lib/model-settings-client.js
1438
1467
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -1447,17 +1476,44 @@ function parseModelSettingsChangeFrame(value) {
1447
1476
  };
1448
1477
  throw new Error("Invalid model settings change frame");
1449
1478
  }
1450
- MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
1451
- ...descriptor,
1452
- result: {
1453
- mode: "strict",
1454
- typeSymbol: "ModelSettings.ChangeFrame",
1455
- create: () => ({ parse: parseModelSettingsChangeFrame })
1456
- }
1457
- }));
1479
+ /** One owner's contribution. Each package mounts only its own endpoint. */
1480
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
1481
+ return {
1482
+ package: packageName,
1483
+ descriptors: [{
1484
+ id: `${identity.namespace}/watch`,
1485
+ service: identity.service,
1486
+ namespace: identity.namespace,
1487
+ method: "watch",
1488
+ mode: "stream",
1489
+ invocation: { kind: "direct" },
1490
+ parameters: [],
1491
+ cancellation: { parameter: "signal" },
1492
+ result: {
1493
+ mode: "strict",
1494
+ typeSymbol: "ModelSettings.ChangeFrame",
1495
+ create: () => ({ parse: parseModelSettingsChangeFrame })
1496
+ }
1497
+ }]
1498
+ };
1499
+ }
1500
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
1501
+ * then withdraws it, so the contribution cannot outlive the plugin. */
1502
+ function mountModelSettingsChanges(ctx, packageName) {
1503
+ const contribution = modelSettingsChangesRemote(packageName);
1504
+ ctx.inject(["remote"], (injected) => {
1505
+ const scope = injected;
1506
+ const mounting = scope.remote.$mount(contribution);
1507
+ mounting.catch(() => {});
1508
+ scope.effect(() => async () => {
1509
+ await (await mounting.catch(() => void 0))?.();
1510
+ });
1511
+ });
1512
+ }
1458
1513
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1459
1514
  * A factory shares one logical stream while at least one subscriber remains. */
1460
- function createModelSettingsSubscriber(ctx, namespace) {
1515
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
1516
+ const endpoint = identity;
1461
1517
  const listeners = /* @__PURE__ */ new Set();
1462
1518
  let injection;
1463
1519
  let current;
@@ -1483,12 +1539,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
1483
1539
  };
1484
1540
  const start = () => {
1485
1541
  const life = ++lifecycle;
1486
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1542
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
1487
1543
  if (life !== lifecycle || !listeners.size) return;
1488
1544
  const scope = injected;
1545
+ const service = scope.remote[endpoint.namespace];
1546
+ if (!service || !("watch" in service) || !service.watch) {
1547
+ report("Model settings sync service unavailable");
1548
+ return;
1549
+ }
1550
+ const watch = service.watch;
1489
1551
  const stream = scope.remote.$stream({
1490
1552
  name: "Model settings sync",
1491
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1553
+ open: (signal) => watch(signal),
1492
1554
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1493
1555
  });
1494
1556
  const session = {
@@ -1523,7 +1585,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
1523
1585
  invalidate();
1524
1586
  } else {
1525
1587
  if (!ready) throw new Error("Model settings sync change arrived before ready");
1526
- if (namespace === "*" || frame.ns === namespace) invalidate();
1588
+ if (accept(frame.ns)) invalidate();
1527
1589
  }
1528
1590
  }
1529
1591
  } catch (error) {
@@ -1844,9 +1906,6 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
1844
1906
  * legend keeps the UA inline padding from indenting the group title. */
1845
1907
  .dsh-safe-auto legend { padding: 0; }
1846
1908
  `;
1847
- function Style() {
1848
- return h("style", null, css);
1849
- }
1850
1909
  function ErrorText({ error, t }) {
1851
1910
  return error ? h("p", {
1852
1911
  role: "alert",
@@ -1939,7 +1998,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1939
1998
  return h("span", {
1940
1999
  className: "dsh-safe-auto dsh-safe-auto-permission",
1941
2000
  "aria-busy": busy
1942
- }, h(Style), h(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2001
+ }, h(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1943
2002
  open,
1944
2003
  side: "top",
1945
2004
  align: "start",
@@ -1978,7 +2037,7 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
1978
2037
  const retained = effort && h("span", null, " ", t("savedEffortRetained", effort));
1979
2038
  return h("fieldset", {
1980
2039
  disabled,
1981
- className: "dsh-ui-card"
2040
+ className: "dsh-safe-auto dsh-ui-card"
1982
2041
  }, h("legend", { className: "dsh-ui-heading" }, t("reviewer")), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("model")), h(ModelMenu, {
1983
2042
  open,
1984
2043
  onOpenChange: setOpen,
@@ -2021,6 +2080,339 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
2021
2080
  clearSearchLabel: t("clearSearch")
2022
2081
  })), state.follow && h("p", { className: "dsh-ui-help" }, t("followHelp"), retained), !state.follow && !state.known && h("p", { className: "dsh-ui-help" }, t("modelAbsent")), !state.follow && unknownEffort && h("p", { className: "dsh-ui-help" }, t("effortAbsent")), !state.follow && h("p", { className: "dsh-ui-help" }, t("effortClears")));
2023
2082
  }
2083
+ const HISTORY_TEXT = {
2084
+ zh: {
2085
+ title: "审核历史",
2086
+ help: "保存启用此功能后的审核。内容已尽力脱敏;记录持续保留,可导出供审计。",
2087
+ search: "搜索内容",
2088
+ session: "会话 ID",
2089
+ tool: "工具",
2090
+ outcome: "审核结果",
2091
+ risk: "风险",
2092
+ from: "开始时间",
2093
+ to: "结束时间",
2094
+ all: "全部",
2095
+ apply: "查询",
2096
+ refresh: "刷新",
2097
+ next: "下一页",
2098
+ back: "上一页",
2099
+ detail: "查看详情",
2100
+ empty: "没有匹配的审核记录",
2101
+ loading: "正在查询…",
2102
+ exportOne: "导出此记录",
2103
+ exportAll: "导出筛选结果",
2104
+ cancel: "取消导出",
2105
+ exporting: (count) => `正在导出 ${count} 条…`,
2106
+ count: (count) => `共 ${count} 条`,
2107
+ missing: "审核记录不存在",
2108
+ execution: "工具结果",
2109
+ approval: "实际审批",
2110
+ approvalUnknown: "未观察到审批结果",
2111
+ unavailable: "未记录",
2112
+ outcomes: {
2113
+ pending: "审核中",
2114
+ interrupted: "审核中断",
2115
+ "allowed-once": "审核通过",
2116
+ rejected: "拒绝",
2117
+ cancelled: "取消",
2118
+ unavailable: "审批不可用"
2119
+ },
2120
+ risks: {
2121
+ low: "低",
2122
+ medium: "中",
2123
+ high: "高",
2124
+ critical: "严重"
2125
+ },
2126
+ states: {
2127
+ "not-observed": "未观察到执行结果",
2128
+ succeeded: "工具返回成功",
2129
+ failed: "工具返回错误"
2130
+ }
2131
+ },
2132
+ en: {
2133
+ title: "Review history",
2134
+ help: "Reviews are retained from the time this feature is enabled. Content is redacted on a best-effort basis and can be exported for audit.",
2135
+ search: "Search contents",
2136
+ session: "Session ID",
2137
+ tool: "Tool",
2138
+ outcome: "Outcome",
2139
+ risk: "Risk",
2140
+ from: "From",
2141
+ to: "To",
2142
+ all: "All",
2143
+ apply: "Search",
2144
+ refresh: "Refresh",
2145
+ next: "Next",
2146
+ back: "Previous",
2147
+ detail: "View details",
2148
+ empty: "No matching reviews",
2149
+ loading: "Loading reviews…",
2150
+ exportOne: "Export this record",
2151
+ exportAll: "Export filtered results",
2152
+ cancel: "Cancel export",
2153
+ exporting: (count) => `Exporting ${count} records…`,
2154
+ count: (count) => `${count} records`,
2155
+ missing: "Review record not found",
2156
+ execution: "Tool result",
2157
+ approval: "Actual approval",
2158
+ approvalUnknown: "No approval result observed",
2159
+ unavailable: "Not recorded",
2160
+ outcomes: {
2161
+ pending: "Pending",
2162
+ interrupted: "Interrupted",
2163
+ "allowed-once": "Review passed",
2164
+ rejected: "Rejected",
2165
+ cancelled: "Cancelled",
2166
+ unavailable: "Approval unavailable"
2167
+ },
2168
+ risks: {
2169
+ low: "Low",
2170
+ medium: "Medium",
2171
+ high: "High",
2172
+ critical: "Critical"
2173
+ },
2174
+ states: {
2175
+ "not-observed": "No execution result observed",
2176
+ succeeded: "Tool returned success",
2177
+ failed: "Tool returned an error"
2178
+ }
2179
+ }
2180
+ };
2181
+ function historyFilters(values) {
2182
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== "").map(([key, value]) => [key, key === "from" || key === "to" ? new Date(value).toISOString() : value]));
2183
+ }
2184
+ function downloadHistory(records, filename = "safe-auto-history.json") {
2185
+ const url = URL.createObjectURL(new Blob([JSON.stringify({
2186
+ format: "safe-auto-audit-v1",
2187
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2188
+ count: records.length,
2189
+ records
2190
+ }, null, 2)], { type: "application/json" }));
2191
+ const link = document.createElement("a");
2192
+ link.href = url;
2193
+ link.download = filename;
2194
+ link.click();
2195
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
2196
+ }
2197
+ function HistoryPanel({ api, locale, initialPage = null, download = downloadHistory }) {
2198
+ const language = (0, react.useSyncExternalStore)((listener) => locale?.subscribe?.(listener) ?? (() => {}), () => localeLanguage(locale), () => localeLanguage(locale));
2199
+ const t = HISTORY_TEXT[language];
2200
+ const [draft, setDraft] = (0, react.useState)({
2201
+ query: "",
2202
+ sessionId: "",
2203
+ tool: "",
2204
+ outcome: "",
2205
+ risk: "",
2206
+ from: "",
2207
+ to: ""
2208
+ });
2209
+ const [filter, setFilter] = (0, react.useState)({});
2210
+ const [page, setPage] = (0, react.useState)(initialPage);
2211
+ const [cursors, setCursors] = (0, react.useState)([void 0]);
2212
+ const [index, setIndex] = (0, react.useState)(0);
2213
+ const [error, setError] = (0, react.useState)("");
2214
+ const [busy, setBusy] = (0, react.useState)(false);
2215
+ const [detail, setDetail] = (0, react.useState)(null);
2216
+ const [exporting, setExporting] = (0, react.useState)(null);
2217
+ const generation = (0, react.useRef)(0), detailsGeneration = (0, react.useRef)(0), exportGeneration = (0, react.useRef)(0);
2218
+ const alive = (0, react.useRef)(true), filterRef = (0, react.useRef)(filter);
2219
+ filterRef.current = filter;
2220
+ const load = (0, react.useCallback)(async (filters, cursor) => {
2221
+ const ticket = ++generation.current;
2222
+ ++detailsGeneration.current;
2223
+ ++exportGeneration.current;
2224
+ setBusy(true);
2225
+ setError("");
2226
+ setDetail(null);
2227
+ setExporting(null);
2228
+ try {
2229
+ const result = await api.historyList({
2230
+ ...filters,
2231
+ limit: 25,
2232
+ ...cursor ? { cursor } : {}
2233
+ });
2234
+ if (alive.current && ticket === generation.current) setPage(result);
2235
+ } catch (e) {
2236
+ if (alive.current && ticket === generation.current) {
2237
+ setError(errorMessage(e));
2238
+ setPage(null);
2239
+ }
2240
+ } finally {
2241
+ if (alive.current && ticket === generation.current) setBusy(false);
2242
+ }
2243
+ }, [api]);
2244
+ (0, react.useEffect)(() => {
2245
+ alive.current = true;
2246
+ load({}, void 0);
2247
+ const stop = api.subscribeGeneration?.(() => {
2248
+ setCursors([void 0]);
2249
+ setIndex(0);
2250
+ load(filterRef.current, void 0);
2251
+ });
2252
+ return () => {
2253
+ alive.current = false;
2254
+ generation.current++;
2255
+ detailsGeneration.current++;
2256
+ exportGeneration.current++;
2257
+ stop?.();
2258
+ };
2259
+ }, [load, api]);
2260
+ const search = (e) => {
2261
+ e?.preventDefault();
2262
+ try {
2263
+ const next = historyFilters(draft);
2264
+ setFilter(next);
2265
+ setCursors([void 0]);
2266
+ setIndex(0);
2267
+ load(next);
2268
+ } catch (e) {
2269
+ setError(errorMessage(e));
2270
+ }
2271
+ };
2272
+ const showDetail = async (id) => {
2273
+ const ticket = ++detailsGeneration.current;
2274
+ setError("");
2275
+ setDetail(null);
2276
+ try {
2277
+ const row = await api.historyGet(id);
2278
+ if (!row) throw new Error(t.missing);
2279
+ if (alive.current && ticket === detailsGeneration.current) setDetail(row);
2280
+ } catch (e) {
2281
+ if (alive.current && ticket === detailsGeneration.current) setError(errorMessage(e));
2282
+ }
2283
+ };
2284
+ const exportAll = async () => {
2285
+ const ticket = ++exportGeneration.current;
2286
+ const records = [];
2287
+ let cursor;
2288
+ setExporting(0);
2289
+ setError("");
2290
+ try {
2291
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2292
+ const filters = {
2293
+ ...filter,
2294
+ to: filter.to && Date.parse(filter.to) < Date.parse(now) ? filter.to : now
2295
+ };
2296
+ do {
2297
+ const batch = await api.historyList({
2298
+ ...filters,
2299
+ limit: 100,
2300
+ ...cursor ? { cursor } : {}
2301
+ });
2302
+ if (!alive.current || ticket !== exportGeneration.current) return;
2303
+ for (const item of batch.items) {
2304
+ const row = await api.historyGet(item.id);
2305
+ if (!alive.current || ticket !== exportGeneration.current) return;
2306
+ if (!row) throw new Error(t.missing);
2307
+ records.push(row);
2308
+ setExporting(records.length);
2309
+ }
2310
+ if (batch.nextCursor && batch.nextCursor === cursor) throw new Error("Invalid history cursor");
2311
+ cursor = batch.nextCursor;
2312
+ } while (cursor);
2313
+ download(records);
2314
+ } catch (e) {
2315
+ if (alive.current && ticket === exportGeneration.current) setError(errorMessage(e));
2316
+ } finally {
2317
+ if (alive.current && ticket === exportGeneration.current) setExporting(null);
2318
+ }
2319
+ };
2320
+ const field = (key, label, options) => h("label", {
2321
+ key,
2322
+ className: "dsh-ui-field"
2323
+ }, h("span", { className: "dsh-ui-label" }, label), options ? h("select", {
2324
+ className: "dsh-ui-select",
2325
+ value: draft[key],
2326
+ onChange: (e) => setDraft({
2327
+ ...draft,
2328
+ [key]: e.target.value
2329
+ })
2330
+ }, h("option", { value: "" }, t.all), ...Object.entries(options).map(([value, label]) => h("option", {
2331
+ key: value,
2332
+ value
2333
+ }, label))) : h(_deepseek_ai_dsh_client_ui_primitives.Input, {
2334
+ type: key === "from" || key === "to" ? "datetime-local" : "text",
2335
+ maxLength: 256,
2336
+ value: draft[key],
2337
+ onChange: (e) => setDraft({
2338
+ ...draft,
2339
+ [key]: e.target.value
2340
+ })
2341
+ }));
2342
+ return h("section", {
2343
+ className: "dsh-ui-stack",
2344
+ "aria-label": t.title,
2345
+ "aria-busy": busy
2346
+ }, h("p", { className: "dsh-ui-help" }, t.help), h("form", {
2347
+ className: "dsh-ui-stack",
2348
+ onSubmit: search
2349
+ }, h("div", { className: "dsh-ui-fields-grid" }, field("query", t.search), field("sessionId", t.session), field("tool", t.tool), field("outcome", t.outcome, t.outcomes), field("risk", t.risk, t.risks), field("from", t.from), field("to", t.to)), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2350
+ type: "submit",
2351
+ variant: "primary"
2352
+ }, t.apply), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2353
+ type: "button",
2354
+ variant: "ghost",
2355
+ onClick: () => {
2356
+ setCursors([void 0]);
2357
+ setIndex(0);
2358
+ load(filter);
2359
+ }
2360
+ }, t.refresh), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2361
+ type: "button",
2362
+ variant: "ghost",
2363
+ disabled: busy || exporting !== null || !page?.total,
2364
+ onClick: exportAll
2365
+ }, t.exportAll))), error && h("p", {
2366
+ role: "alert",
2367
+ className: "dsh-ui-error"
2368
+ }, error), exporting !== null && h("div", {
2369
+ className: "dsh-ui-actions",
2370
+ role: "status"
2371
+ }, t.exporting(exporting), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2372
+ variant: "ghost",
2373
+ onClick: () => {
2374
+ exportGeneration.current++;
2375
+ setExporting(null);
2376
+ }
2377
+ }, t.cancel)), busy ? h("p", { role: "status" }, t.loading) : page && h(react.default.Fragment, null, h("p", { className: "dsh-ui-meta" }, t.count(page.total)), !page.items.length && h("p", { className: "dsh-ui-empty" }, t.empty), ...page.items.map((row) => h("article", {
2378
+ key: row.id,
2379
+ className: "dsh-ui-card dsh-ui-stack"
2380
+ }, h("div", { className: "dsh-ui-row-wrap" }, h("strong", null, row.tool), h("span", null, t.outcomes[row.reviewOutcome || row.outcome] || row.reviewOutcome || row.outcome), h("span", null, t.risks[row.risk] || row.risk || "")), h("p", { className: "dsh-ui-meta dsh-ui-wrap" }, new Date(row.startedAt).toLocaleString(language), " · ", row.sessionId, " · ", row.reviewer ? `${row.reviewer.provider}/${row.reviewer.model}` : t.unavailable), row.rationale && h("p", { className: "dsh-ui-wrap" }, row.rationale), h("p", { className: "dsh-ui-help" }, t.approval, ": ", !row.approvalState || row.approvalState === "not-observed" ? t.approvalUnknown : row.approvalState === "allowed-once" ? language === "zh" ? "单次放行" : "Allowed once" : t.outcomes[row.approvalState]), h("p", { className: "dsh-ui-help" }, t.execution, ": ", t.states[row.executionState || "not-observed"]), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2381
+ variant: "ghost",
2382
+ onClick: () => showDetail(row.id)
2383
+ }, t.detail))), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2384
+ variant: "ghost",
2385
+ disabled: index === 0,
2386
+ onClick: () => {
2387
+ const next = index - 1;
2388
+ setIndex(next);
2389
+ load(filter, cursors[next]);
2390
+ }
2391
+ }, t.back), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2392
+ variant: "ghost",
2393
+ disabled: !page.nextCursor,
2394
+ onClick: () => {
2395
+ const next = index + 1;
2396
+ setCursors([...cursors.slice(0, next), page.nextCursor]);
2397
+ setIndex(next);
2398
+ load(filter, page.nextCursor);
2399
+ }
2400
+ }, t.next))), detail && h("section", {
2401
+ className: "dsh-ui-stack",
2402
+ "aria-label": t.detail
2403
+ }, h("h3", null, detail.id), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2404
+ variant: "ghost",
2405
+ onClick: () => download([detail], `safe-auto-${detail.id}.json`)
2406
+ }, t.exportOne), h("pre", {
2407
+ className: "dsh-ui-wrap",
2408
+ style: {
2409
+ whiteSpace: "pre-wrap",
2410
+ overflowWrap: "anywhere",
2411
+ maxHeight: "60vh",
2412
+ overflow: "auto"
2413
+ }
2414
+ }, JSON.stringify(detail, null, 2))));
2415
+ }
2024
2416
  function SettingsPanel({ view, api, locale, initialSettings = null, initialCatalog = null, initialTab = "reviewer" }) {
2025
2417
  const t = useText(locale);
2026
2418
  const [settings, setSettings] = (0, react.useState)(initialSettings);
@@ -2171,21 +2563,22 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2171
2563
  tabIndex: 0,
2172
2564
  className
2173
2565
  }, children);
2174
- if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, {
2566
+ if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, {
2175
2567
  error,
2176
2568
  t
2177
2569
  }));
2178
2570
  return h("section", {
2179
- className: "dsh-safe-auto dsh-ui-stack",
2571
+ className: "dsh-safe-auto dsh-ui-page",
2180
2572
  "aria-label": t("panelLabel"),
2181
2573
  "aria-busy": busy
2182
- }, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("intro")), !values && h("p", { className: "dsh-ui-loading" }, t("loading")), values && h(react.default.Fragment, null, h(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
2574
+ }, h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("intro")), !values && h("p", { className: "dsh-ui-loading" }, t("loading")), values && h(react.default.Fragment, null, h(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
2183
2575
  value: activeTab,
2184
2576
  onChange: setTab,
2185
2577
  label: t("tabsLabel"),
2186
2578
  items: [
2187
2579
  tabItem("reviewer", t("tabReviewer")),
2188
2580
  tabItem("details", t("details")),
2581
+ tabItem("history", HISTORY_TEXT[localeLanguage(locale)].title),
2189
2582
  ...settings?.policyLimits ? [tabItem("limits", t("tabLimits"))] : []
2190
2583
  ]
2191
2584
  }), tabPanel("reviewer", "dsh-ui-stack", [
@@ -2217,13 +2610,16 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2217
2610
  id: "sa-prompt-hint",
2218
2611
  className: "dsh-ui-help"
2219
2612
  }, t("promptHint", (values.reviewerPrompt || "").length))
2220
- ]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("reviewInput")), h("p", null, t("breakerDetail")))), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [h("p", {
2613
+ ]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("reviewInput")), h("p", null, t("breakerDetail")))), tabPanel("history", "dsh-ui-stack", activeTab === "history" ? h(HistoryPanel, {
2614
+ api,
2615
+ locale
2616
+ }) : null), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [h("p", {
2221
2617
  key: "line",
2222
2618
  className: "dsh-ui-meta dsh-ui-wrap"
2223
2619
  }, t("limitsLine", settings.policyLimits)), h("p", {
2224
2620
  key: "help",
2225
2621
  className: "dsh-ui-help"
2226
- }, t("limitsHelp"))]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2622
+ }, t("limitsHelp"))]), activeTab === "reviewer" && h("div", { className: "dsh-ui-form-footer" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2227
2623
  variant: "primary",
2228
2624
  disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
2229
2625
  onClick: save
@@ -2249,6 +2645,15 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2249
2645
  }, t("saved")));
2250
2646
  }
2251
2647
  function apply(ctx) {
2648
+ ctx.effect(() => {
2649
+ if (typeof document === "undefined") return void 0;
2650
+ const style = document.createElement("style");
2651
+ style.setAttribute("data-plugin", BUNDLE);
2652
+ style.textContent = css;
2653
+ document.head.appendChild(style);
2654
+ return () => style.remove();
2655
+ });
2656
+ mountModelSettingsChanges(ctx, BUNDLE);
2252
2657
  const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
2253
2658
  const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
2254
2659
  const api = Object.freeze({
@@ -2257,6 +2662,8 @@ function apply(ctx) {
2257
2662
  getSettings: () => call("settings.get", {}),
2258
2663
  saveSettings: (payload) => call("settings.save", payload),
2259
2664
  modelCatalog: () => readModelCatalog(ctx),
2665
+ historyList: (payload) => call("history.list", payload),
2666
+ historyGet: (id) => call("history.get", { id }),
2260
2667
  getSession: (sessionId) => call("session.get", { sessionId }),
2261
2668
  selectSession: (payload) => call("session.select", payload)
2262
2669
  });
@@ -2280,12 +2687,15 @@ function apply(ctx) {
2280
2687
  //#endregion
2281
2688
  exports.BUNDLE = BUNDLE;
2282
2689
  exports.CHANNEL = CHANNEL;
2690
+ exports.HistoryPanel = HistoryPanel;
2283
2691
  exports.PermissionMenu = PermissionMenu;
2284
2692
  exports.ReviewerFields = ReviewerFields;
2285
2693
  exports.SettingsPanel = SettingsPanel;
2286
2694
  exports.apply = apply;
2287
2695
  exports.createRequestScope = createRequestScope;
2696
+ exports.downloadHistory = downloadHistory;
2288
2697
  exports.errorText = errorText;
2698
+ exports.historyFilters = historyFilters;
2289
2699
  exports.inject = inject;
2290
2700
  exports.localeLanguage = localeLanguage;
2291
2701
  exports.name = name;