@klarkxy/dsh-safe-auto 0.4.0 → 0.5.1

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
@@ -1870,9 +1906,6 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
1870
1906
  * legend keeps the UA inline padding from indenting the group title. */
1871
1907
  .dsh-safe-auto legend { padding: 0; }
1872
1908
  `;
1873
- function Style() {
1874
- return h("style", null, css);
1875
- }
1876
1909
  function ErrorText({ error, t }) {
1877
1910
  return error ? h("p", {
1878
1911
  role: "alert",
@@ -1965,7 +1998,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1965
1998
  return h("span", {
1966
1999
  className: "dsh-safe-auto dsh-safe-auto-permission",
1967
2000
  "aria-busy": busy
1968
- }, h(Style), h(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2001
+ }, h(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1969
2002
  open,
1970
2003
  side: "top",
1971
2004
  align: "start",
@@ -2004,7 +2037,7 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
2004
2037
  const retained = effort && h("span", null, " ", t("savedEffortRetained", effort));
2005
2038
  return h("fieldset", {
2006
2039
  disabled,
2007
- className: "dsh-ui-card"
2040
+ className: "dsh-safe-auto dsh-ui-card"
2008
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, {
2009
2042
  open,
2010
2043
  onOpenChange: setOpen,
@@ -2047,6 +2080,339 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
2047
2080
  clearSearchLabel: t("clearSearch")
2048
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")));
2049
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
+ }
2050
2416
  function SettingsPanel({ view, api, locale, initialSettings = null, initialCatalog = null, initialTab = "reviewer" }) {
2051
2417
  const t = useText(locale);
2052
2418
  const [settings, setSettings] = (0, react.useState)(initialSettings);
@@ -2197,21 +2563,22 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2197
2563
  tabIndex: 0,
2198
2564
  className
2199
2565
  }, children);
2200
- 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, {
2201
2567
  error,
2202
2568
  t
2203
2569
  }));
2204
2570
  return h("section", {
2205
- className: "dsh-safe-auto dsh-ui-stack",
2571
+ className: "dsh-safe-auto dsh-ui-page",
2206
2572
  "aria-label": t("panelLabel"),
2207
2573
  "aria-busy": busy
2208
- }, 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, {
2209
2575
  value: activeTab,
2210
2576
  onChange: setTab,
2211
2577
  label: t("tabsLabel"),
2212
2578
  items: [
2213
2579
  tabItem("reviewer", t("tabReviewer")),
2214
2580
  tabItem("details", t("details")),
2581
+ tabItem("history", HISTORY_TEXT[localeLanguage(locale)].title),
2215
2582
  ...settings?.policyLimits ? [tabItem("limits", t("tabLimits"))] : []
2216
2583
  ]
2217
2584
  }), tabPanel("reviewer", "dsh-ui-stack", [
@@ -2243,13 +2610,16 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2243
2610
  id: "sa-prompt-hint",
2244
2611
  className: "dsh-ui-help"
2245
2612
  }, t("promptHint", (values.reviewerPrompt || "").length))
2246
- ]), 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", {
2247
2617
  key: "line",
2248
2618
  className: "dsh-ui-meta dsh-ui-wrap"
2249
2619
  }, t("limitsLine", settings.policyLimits)), h("p", {
2250
2620
  key: "help",
2251
2621
  className: "dsh-ui-help"
2252
- }, 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, {
2253
2623
  variant: "primary",
2254
2624
  disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
2255
2625
  onClick: save
@@ -2275,6 +2645,14 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2275
2645
  }, t("saved")));
2276
2646
  }
2277
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
+ });
2278
2656
  mountModelSettingsChanges(ctx, BUNDLE);
2279
2657
  const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
2280
2658
  const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
@@ -2284,6 +2662,8 @@ function apply(ctx) {
2284
2662
  getSettings: () => call("settings.get", {}),
2285
2663
  saveSettings: (payload) => call("settings.save", payload),
2286
2664
  modelCatalog: () => readModelCatalog(ctx),
2665
+ historyList: (payload) => call("history.list", payload),
2666
+ historyGet: (id) => call("history.get", { id }),
2287
2667
  getSession: (sessionId) => call("session.get", { sessionId }),
2288
2668
  selectSession: (payload) => call("session.select", payload)
2289
2669
  });
@@ -2307,12 +2687,15 @@ function apply(ctx) {
2307
2687
  //#endregion
2308
2688
  exports.BUNDLE = BUNDLE;
2309
2689
  exports.CHANNEL = CHANNEL;
2690
+ exports.HistoryPanel = HistoryPanel;
2310
2691
  exports.PermissionMenu = PermissionMenu;
2311
2692
  exports.ReviewerFields = ReviewerFields;
2312
2693
  exports.SettingsPanel = SettingsPanel;
2313
2694
  exports.apply = apply;
2314
2695
  exports.createRequestScope = createRequestScope;
2696
+ exports.downloadHistory = downloadHistory;
2315
2697
  exports.errorText = errorText;
2698
+ exports.historyFilters = historyFilters;
2316
2699
  exports.inject = inject;
2317
2700
  exports.localeLanguage = localeLanguage;
2318
2701
  exports.name = name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klarkxy/dsh-safe-auto",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "LICENSE"
24
24
  ],
25
25
  "scripts": {
26
- "build": "node --check src/config.js && node --check src/dangerous.js && node --check src/transcript.js && node --check src/model-route.js && node --check src/reviewer.js && node --check src/index.js && node --check src/control.js && node --check src/ui-host.js && tsdown && node ../../scripts/editor-plugins/wrap-client.mjs @klarkxy/dsh-safe-auto",
26
+ "build": "node --check src/config.js && node --check src/dangerous.js && node --check src/transcript.js && node --check src/model-route.js && node --check src/reviewer.js && node --check src/audit.js && node --check src/index.js && node --check src/control.js && node --check src/ui-host.js && tsdown && node ../../scripts/editor-plugins/wrap-client.mjs @klarkxy/dsh-safe-auto",
27
27
  "test": "node --test test/*.test.js"
28
28
  },
29
29
  "engines": {
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "dependencies": {
61
- "@klarkxy/dsh-plugin-kit": "0.4.0",
61
+ "@klarkxy/dsh-plugin-kit": "0.5.0",
62
62
  "zod": "^4.1.5"
63
63
  },
64
64
  "peerDependencies": {
@@ -81,7 +81,7 @@
81
81
  "@deepseek-ai/cordis": "4.0.5",
82
82
  "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
83
83
  "@deepseek-ai/dsh-storage-domain": "0.2.1-rc.1",
84
- "@klarkxy/dsh-model-route": "0.2.5",
84
+ "@klarkxy/dsh-model-route": "0.2.6",
85
85
  "jsdom": "29.1.1",
86
86
  "@deepseek-ai/dsh-working-directory": "0.2.1-rc.1"
87
87
  },
@@ -90,6 +90,6 @@
90
90
  "registry": "https://registry.npmjs.org/"
91
91
  },
92
92
  "dshRelease": {
93
- "contentHash": "fbbaa607cab540cfc58b7d99946f5a0954f3b4c51b9ceb90e452fb3fd0522bed"
93
+ "contentHash": "f1c3c6a5282fcee7e6ae41538bd2c87f4eaa362ec2165412a4b5292d46f430ff"
94
94
  }
95
95
  }