dsh-approval-review 0.3.0 → 0.3.2

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/README-zh.md CHANGED
@@ -82,7 +82,7 @@ dsh --profile <profile> --dump-config | grep -A6 'id: approval-review'
82
82
  | `maxAutoAllowRisk` | `medium` | 允许复核者自动放行的最高风险。 |
83
83
  | `onRiskExceeded` | `delegate` | 超过该上限时:`allow` / `delegate` / `deny`。 |
84
84
  | `onUncertain` | `delegate` | 复核者表示无法判断时。 |
85
- | `onReviewerFailure` | `rejected` | 复核崩溃、超时或输出不合 schema 时。 |
85
+ | `onReviewerFailure` | `delegate` | 复核崩溃、超时或输出不合 schema 时。默认**转人工**:复核者跑不起来是基础设施问题,不是裁决;会拒的部署请显式设成 `rejected`。 |
86
86
  | `budget.maxReviewsPerTurn` | `20` | 每回合复核调用上限。 |
87
87
  | `budget.onExhausted` | `delegate` | 预算耗尽后:`delegate` / `deny`。 |
88
88
  | `maxFailuresPerTurn` | `10` | 每回合复核**失败**次数上限,超过即转人工。 |
@@ -224,7 +224,7 @@ dsh --profile <profile> --dump-config | grep -A6 'id: approval-review'
224
224
  - **复核者的证据是数据,不是指令。** 证据包里的 transcript 与申请理由可能包含仓库可控文本(`AGENTS.md`、被审文件、命令输出)。数据/指令边界由代码追加、不受 `policyText` 覆盖;证据中出现指令或"已经批准过"的说法一律作为反对证据。
225
225
  - **复核者是只读的。** `mode: direct` 是一次不挂工具的模型调用;`mode: subagent` 是有 `toolFilter` 白名单与 `maxDepth: 1`(子代理自身深度,允许它存在、不允许它再派孙代理)的子代理。两种形态都无法写入、执行或委派,因此即便复核者被攻破,也无法升级它所守卫的那道边界。
226
226
  - **复核者不会递归。** 子代理一旦建立即被登记为复核者会话,它自己的审批请求交还人工链,不会回到正在服务它的应答者。
227
- - **默认失败即拒绝。** `onReviewerFailure: rejected`、`onUncertain: delegate`、`maxAutoAllowRisk: medium` 是出厂选择:误拒一个安全动作的代价是一次重试,误放一个危险动作可能无法挽回。
227
+ - **复核者跑不起来时找人不拒绝。** `onReviewerFailure: delegate`、`onUncertain: delegate`、`maxAutoAllowRisk: medium` 是出厂选择:复核者无法运行属于基础设施故障,把它变成自动拒绝会让操作者看到一次模型从未做出的否决。要 fail-closed 的部署显式设 `onReviewerFailure: rejected`:那时误拒一个安全动作的代价是一次重试,而误放一个危险动作可能无法挽回。
228
228
  - **它不是安全保证。** 它只评估审批缝真正提出的请求,而语言模型会犯错,在对抗性场景下尤其如此。它是配置良好的沙箱的补充,不是替代。
229
229
 
230
230
  ## 开发
package/README.md CHANGED
@@ -102,7 +102,7 @@ schema defaults.
102
102
  | `maxAutoAllowRisk` | `medium` | Highest risk the reviewer may auto-allow. |
103
103
  | `onRiskExceeded` | `delegate` | `allow` / `delegate` / `deny` above that ceiling. |
104
104
  | `onUncertain` | `delegate` | Reviewer reported it could not decide. |
105
- | `onReviewerFailure` | `rejected` | Reviewer crashed, timed out, or answered off-schema. |
105
+ | `onReviewerFailure` | `delegate` | Reviewer crashed, timed out, or answered off-schema. Defaults to **delegating**: a reviewer that could not run is an infrastructure problem, not a verdict — set `rejected` for the fail-closed stance. |
106
106
  | `budget.maxReviewsPerTurn` | `20` | Reviewer calls per open turn. |
107
107
  | `budget.onExhausted` | `delegate` | `delegate` / `deny` once spent. |
108
108
  | `maxFailuresPerTurn` | `10` | Reviewer *failures* per open turn before requests delegate. |
@@ -304,8 +304,10 @@ reconstructible from the log alone.
304
304
  - **The reviewer cannot recurse.** A reviewer child is registered as soon as it
305
305
  exists, so its own approval asks are delegated to the human chain instead of
306
306
  returning to the answerer serving it.
307
- - **Fail closed by default.** `onReviewerFailure: rejected`, `onUncertain:
308
- delegate`, and `maxAutoAllowRisk: medium` are the shipping choices because
307
+ - **A reviewer that cannot run asks a human.** `onReviewerFailure: delegate`,
308
+ `onUncertain: delegate`, and `maxAutoAllowRisk: medium` are the shipping
309
+ choices: refusing in the model's name would make an infrastructure failure
310
+ look like a judgement. Set `onReviewerFailure: rejected` for fail-closed, where
309
311
  refusing a safe action costs a retry while approving an unsafe one may be
310
312
  unrecoverable.
311
313
  - **It is not a security guarantee.** It evaluates only the requests the approval
package/cordis.patch.yml CHANGED
@@ -62,9 +62,14 @@
62
62
  onRiskExceeded: delegate
63
63
  # Reviewer said it could not decide.
64
64
  onUncertain: delegate
65
- # Reviewer crashed, timed out, or answered off-schema. `rejected` is the
66
- # fail-closed stance; `delegate` hands the request to the human chain.
67
- onReviewerFailure: rejected
65
+ # Reviewer crashed, timed out, or answered off-schema.
66
+ #
67
+ # `delegate` hands the request to the human chain: a reviewer that could
68
+ # not run is an INFRASTRUCTURE problem (a dead route, an expired
69
+ # credential, a timeout), and turning that into an automatic refusal
70
+ # makes the operator see a denial the model never made. `rejected` is the
71
+ # fail-closed alternative for deployments that prefer refusing to asking.
72
+ onReviewerFailure: delegate
68
73
  # Per-turn reviewer budget, so a loop cannot bill unlimited reviews.
69
74
  budget:
70
75
  maxReviewsPerTurn: 20
package/lib/client.js CHANGED
@@ -834,7 +834,17 @@ window.__ModuleLoader__.load({
834
834
  "button[role=\"menuitem\"]",
835
835
  `[${MARK_ATTRIBUTE}]`
836
836
  ].join(", ");
837
- /** Build the stylesheet that draws the mark on a decorated button. */
837
+ /**
838
+ * Build the stylesheet that draws the mark on a decorated button.
839
+ *
840
+ * The colour is not cosmetic guesswork: the built-in glyphs sit inside the
841
+ * menu's `.itemIcon` span, which the design system tints
842
+ * `--dsw-alias-label-tertiary`, while the button itself is
843
+ * `--dsw-alias-label-primary` (brighter, and brighter still on the selected
844
+ * row). A `::before` on the button therefore inherits the WRONG one, which is
845
+ * why the mark looked white next to three grey shields. The trigger has no such
846
+ * span — its built-in icon inherits the button — so it keeps `currentColor`.
847
+ */
838
848
  function stylesheet() {
839
849
  const mask = `url("data:image/svg+xml,${encodeURIComponent(GLYPH_SVG)}")`;
840
850
  return `
@@ -854,7 +864,11 @@ window.__ModuleLoader__.load({
854
864
  -webkit-mask-size:contain;
855
865
  mask-size:contain;
856
866
  }
857
- /* The composer trigger sizes its icons at 14px. */
867
+ /* A menu row's icon slot is tinted tertiary; match it instead of the button. */
868
+ button[role="menuitem"][${MARK_ATTRIBUTE}]::before{
869
+ background-color:var(--dsw-alias-label-tertiary, currentColor);
870
+ }
871
+ /* The composer trigger sizes its icons at 14px and lets them inherit. */
858
872
  button[aria-label][${MARK_ATTRIBUTE}]::before{
859
873
  width:14px;
860
874
  height:14px;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["TEXT","MUTED","BORDER","PANEL","CODE","useState","useRef","useMemo","useState","useRef","useMemo"],"sources":["../src/client/scroll.ts","../src/client/model-choices.ts","../src/client/ModelPicker.tsx","../src/client/LedgerView.tsx","../src/client/access-mode-glyph.ts","../src/client/run-command.ts","../src/client/index.tsx"],"sourcesContent":["/**\n * Scroll positioning for a view rendered inside someone else's scrollport.\n *\n * The Approvals tab is mounted inside the conversation's resident scrollport\n * (`.scrollBody`, `overflow-y: auto`), which the transcript keeps pinned to its\n * newest line. Switching tabs does not reset that box, so a ledger mounted under\n * it opens at its own BOTTOM — the opposite of useful when the newest decision\n * is the first row.\n *\n * A plugin cannot claim that scrollport: an intermediate slot element breaks the\n * height chain a `height: 100%; overflow: auto` root would need to become the\n * scroller itself. Resetting the nearest scrollable ancestor is the fix, and the\n * decision of WHICH ancestor that is lives here so it can be tested without a\n * browser.\n * @module dsh-approval-review/client/scroll\n */\n\n/** The minimal element surface the walk needs. */\nexport interface ScrollableNode {\n /** Parent in the element tree, or null at the root. */\n readonly parentElement: ScrollableNode | null\n /** Full content height. */\n readonly scrollHeight: number\n /** Visible height. */\n readonly clientHeight: number\n /** Current scroll offset; assigned when this node is chosen. */\n scrollTop: number\n}\n\n/** Bound on the ancestor walk, so a pathological tree cannot spin. */\nexport const ANCESTOR_WALK_LIMIT = 12\n\n/**\n * Reset the nearest scrollable ancestor that actually overflows.\n *\n * \"Actually overflows\" matters: an ancestor with `overflow-y: auto` but no\n * overflow cannot be scrolled, so resetting it is a no-op that would hide the\n * real scroller further up.\n * @param from - the element whose ancestry to search (usually the view root).\n * @param overflowYOf - computed `overflow-y` accessor for one node.\n * @param maxDepth - stop after this many ancestors.\n * @returns the node that was reset, or undefined when none qualified.\n */\nexport function resetScrollableAncestorToTop<T extends ScrollableNode>(\n from: T | null,\n overflowYOf: (node: T) => string,\n maxDepth: number = ANCESTOR_WALK_LIMIT,\n): T | undefined {\n let node: ScrollableNode | null = from?.parentElement ?? null\n for (let depth = 0; node !== null && depth < maxDepth; depth += 1, node = node.parentElement) {\n const overflowY = overflowYOf(node as T)\n if (overflowY !== 'auto' && overflowY !== 'scroll') continue\n if (node.scrollHeight <= node.clientHeight) continue\n node.scrollTop = 0\n return node as T\n }\n return undefined\n}\n","/**\n * Reviewer-route choices for the Approvals tab's model picker.\n *\n * The reviewer runs as a subagent, so the routes a deployment actually offers it\n * are already published as session projections — this module just reads them\n * instead of hardcoding a model list that would go stale:\n *\n * - `subagentModelSelectionPolicy`: the deployment's allowed subagent routes\n * (`subagent-model-selection.allowedModels` in `settings.yaml`);\n * - `modelSelection.lastUsed`: the session's own route, i.e. what \"inherit\"\n * resolves to.\n *\n * Everything is read structurally and defensively: a projection this host does\n * not publish, or an entry with a non-string half, contributes nothing rather\n * than breaking the picker.\n * @module dsh-approval-review/client/model-choices\n */\n\n/** One `{provider, model}` route, when both halves are strings. */\nfunction routeOf(value: unknown): string | undefined {\n if (typeof value !== 'object' || value === null) return undefined\n const entry = value as { readonly provider?: unknown; readonly model?: unknown }\n if (typeof entry.provider !== 'string' || typeof entry.model !== 'string') return undefined\n if (entry.provider.length === 0 || entry.model.length === 0) return undefined\n return `${entry.provider}/${entry.model}`\n}\n\n/** Every valid route in a projection that carries a list of them. */\nfunction routesOf(value: unknown): readonly string[] {\n if (!Array.isArray(value)) return []\n const out: string[] = []\n for (const entry of value) {\n const route = routeOf(entry)\n if (route !== undefined) out.push(route)\n }\n return out\n}\n\n/**\n * Routes AND their display names from a client model-directory snapshot.\n *\n * The route id is what a command needs, but it is NOT the vendor's model name:\n * `deepseek-official/deepseek-flash` is the harness catalog's id whose display\n * name is \"DeepSeek-V41-Flash\", and a deployment's own providers name their\n * routes freely. Showing both is what stops \"that model is not in my\n * subscription list\" from being a mystery.\n * @param value - the directory state returned by `directoryFor(session).load()`.\n * @returns distinct route ids (catalog order) plus their display names.\n */\nexport function directoryRoutes(value: unknown): {\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n} {\n const routes: string[] = []\n const labels: Record<string, string> = {}\n if (typeof value !== 'object' || value === null) return { routes, labels }\n const groups = (value as { readonly groups?: unknown }).groups\n if (!Array.isArray(groups)) return { routes, labels }\n for (const group of groups) {\n if (typeof group !== 'object' || group === null) continue\n const id = (group as { readonly id?: unknown }).id\n const models = (group as { readonly models?: unknown }).models\n if (typeof id !== 'string' || id.length === 0 || !Array.isArray(models)) continue\n for (const model of models) {\n if (typeof model !== 'object' || model === null) continue\n const modelId = (model as { readonly id?: unknown }).id\n if (typeof modelId !== 'string' || modelId.length === 0) continue\n const route = `${id}/${modelId}`\n if (!routes.includes(route)) routes.push(route)\n const name = (model as { readonly name?: unknown }).name\n if (typeof name === 'string' && name.length > 0 && name !== modelId) labels[route] = name\n }\n }\n return { routes, labels }\n}\n\n/**\n * Every route in a client model-directory snapshot (`modelDirectories` service).\n *\n * This is the SAME catalog the composer's model seat and the `/model` picker\n * read, so the reviewer picker offers exactly the models the deployment\n * configures locally — minus anything the catalog failed to load, which it\n * reports separately and which we deliberately do not guess at.\n * @param value - the directory state returned by `directoryFor(session).load()`.\n * @returns distinct `provider/model` labels in catalog order.\n */\nexport function routesFromDirectory(value: unknown): readonly string[] {\n return directoryRoutes(value).routes\n}\n\n/**\n * Build the picker's option list.\n *\n * Order is deliberate: the session override in force first (so a route chosen\n * outside the deployment's list still shows as the current selection), then the\n * session's own model, then the deployment's allowed subagent routes. Duplicates\n * collapse, so a model that is both the session default and an allowed route\n * appears once.\n * @param input - the projections' raw values plus the override in force.\n * @returns distinct `provider/model` labels, in display order.\n */\nexport function reviewerRouteChoices(input: {\n /** The session override in force, as `provider/model`, when one is set. */\n readonly current?: string | undefined\n /** Raw `modelSelection` projection value. */\n readonly sessionDefault?: unknown\n /** Raw `subagentModelSelectionPolicy` projection value. */\n readonly allowed?: unknown\n}): readonly string[] {\n const out: string[] = []\n const push = (route: string | undefined): void => {\n if (route === undefined || route.length === 0 || out.includes(route)) return\n out.push(route)\n }\n push(input.current)\n const session = typeof input.sessionDefault === 'object' && input.sessionDefault !== null\n ? (input.sessionDefault as { readonly lastUsed?: unknown }).lastUsed\n : undefined\n push(routeOf(session))\n for (const route of routesOf(input.allowed)) push(route)\n return out\n}\n\n/**\n * Filter routes for the picker's list.\n *\n * Matching is a case-insensitive substring over the whole `provider/model`\n * label, so typing `luna` and typing `codex/luna` both narrow to the same row.\n * An empty (or whitespace-only) query keeps the whole list.\n * @param routes - candidate labels.\n * @param query - what the operator typed.\n * @returns the matching labels, in input order.\n */\nexport function filterRoutes(routes: readonly string[], query: string): readonly string[] {\n const needle = query.trim().toLowerCase()\n if (needle.length === 0) return routes\n return routes.filter(route => route.toLowerCase().includes(needle))\n}\n","/**\n * The reviewer-model picker.\n *\n * A native `<datalist>` (or `<select>`) popup is drawn by the browser, not the\n * page: its font, weight, and width ignore CSS entirely, which made the list\n * read as a different, much louder control than the tab it sits in. This is the\n * plugin's own listbox instead, styled with the ledger's own type scale, with a\n * free-text field on top so an id the catalog no longer advertises stays\n * reachable (catalog membership is advisory).\n * @module dsh-approval-review/client/ModelPicker\n */\n\nimport { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'\nimport { filterRoutes } from './model-choices.ts'\n\n/** Props for the picker. */\nexport interface ModelPickerProps {\n /** Routes this deployment offers, in display order. */\n readonly choices: readonly string[]\n /**\n * Route id → the catalog's display name (\"DeepSeek-V41-Flash\" for\n * `deepseek-official/deepseek-flash`). The id is what the command needs, but it\n * is not the vendor's model name, and that mismatch reads as a bug.\n */\n readonly labels?: Readonly<Record<string, string>> | undefined\n /** Apply one shell command line (the picker emits `/approval-review model …`). */\n readonly runCommand: (line: string) => void\n /** Load the catalog on first use. */\n readonly loadModelRoutes?: (() => Promise<{\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n }>) | undefined\n /** Whether to render copy in Chinese. */\n readonly zh: boolean\n /** Replaces the base list once the catalog arrives; base first. */\n readonly onRoutesLoaded: (loaded: {\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n }) => void\n}\n\nconst TEXT = 'var(--dsw-alias-label-primary, #e6edf3)'\nconst MUTED = 'var(--dsw-alias-label-tertiary, #8b949e)'\nconst BORDER = 'var(--dsw-alias-border-l2, #30363d)'\nconst PANEL = 'var(--dsw-alias-bg-layer-2, #161b22)'\nconst HOVER = 'var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.08))'\nconst CODE = 'var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)'\n\n/** The picker's field and its plugin-rendered list. */\nexport function ModelPicker({ choices, labels, runCommand, loadModelRoutes, zh, onRoutesLoaded }: ModelPickerProps): React.JSX.Element {\n const [draft, setDraft] = useState('')\n const [open, setOpen] = useState(false)\n const [highlight, setHighlight] = useState(0)\n const rootRef = useRef<HTMLSpanElement>(null)\n const requestedRef = useRef(false)\n\n const matches = useMemo(() => filterRoutes(choices, draft), [choices, draft])\n\n /** Fetch the catalog once, on first interaction. */\n const loadOnce = (): void => {\n if (requestedRef.current || loadModelRoutes === undefined) return\n requestedRef.current = true\n void loadModelRoutes()\n .then(loaded => onRoutesLoaded(loaded))\n .catch(() => onRoutesLoaded({ routes: [], labels: {} }))\n }\n\n useEffect(() => {\n if (!open) return\n const onPointerDown = (event: MouseEvent): void => {\n if (rootRef.current?.contains(event.target as Node) === true) return\n setOpen(false)\n }\n document.addEventListener('mousedown', onPointerDown)\n return () => { document.removeEventListener('mousedown', onPointerDown) }\n }, [open])\n\n const apply = (route: string): void => {\n const value = route.trim()\n if (value.length === 0) return\n runCommand(`/approval-review model ${value}`)\n setDraft('')\n setOpen(false)\n }\n\n const fieldStyle: CSSProperties = {\n fontFamily: CODE,\n fontSize: 11,\n lineHeight: '16px',\n padding: '2px 6px',\n width: 190,\n borderRadius: 6,\n border: `1px solid ${BORDER}`,\n background: 'transparent',\n color: TEXT,\n outline: 'none',\n }\n\n /** One row's text: the id that will be sent, plus the name a human recognizes. */\n const textOf = (route: string): string => {\n const name = labels?.[route]\n return name === undefined ? route : `${route} · ${name}`\n }\n\n const itemStyle: CSSProperties = {\n display: 'block',\n width: '100%',\n textAlign: 'left',\n background: 'transparent',\n border: 'none',\n borderRadius: 4,\n cursor: 'pointer',\n // The ledger's own type scale: this list is chrome, not a headline.\n fontFamily: CODE,\n fontSize: 11,\n fontWeight: 400,\n lineHeight: '16px',\n padding: '3px 8px',\n color: TEXT,\n }\n\n return (\n <span ref={rootRef} style={{ position: 'relative', display: 'inline-flex' }}>\n <input\n value={draft}\n role=\"combobox\"\n aria-expanded={open}\n aria-label={zh ? '复核模型' : 'reviewer model'}\n placeholder={zh ? '选择或输入模型' : 'pick or type a model'}\n style={fieldStyle}\n onFocus={() => { loadOnce(); setOpen(true) }}\n onClick={() => { loadOnce(); setOpen(true) }}\n onChange={(event) => { setDraft(event.target.value); setHighlight(0); setOpen(true) }}\n onKeyDown={(event) => {\n if (event.key === 'Escape') { setOpen(false); return }\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n if (matches.length === 0) return\n setOpen(true)\n setHighlight(current => {\n const next = event.key === 'ArrowDown' ? current + 1 : current - 1\n return (next + matches.length) % matches.length\n })\n return\n }\n if (event.key !== 'Enter') return\n // Enter takes the highlighted suggestion when the list is open, and\n // otherwise applies exactly what was typed.\n const picked = open ? matches[highlight] : undefined\n apply(picked ?? draft)\n }}\n />\n {open ? (\n <span\n role=\"listbox\"\n style={{\n position: 'absolute',\n top: 'calc(100% + 4px)',\n left: 0,\n zIndex: 60,\n minWidth: '100%',\n maxWidth: 320,\n maxHeight: 220,\n overflowY: 'auto',\n background: PANEL,\n border: `1px solid ${BORDER}`,\n borderRadius: 8,\n padding: 4,\n boxShadow: '0 8px 24px rgba(0,0,0,.45)',\n }}\n >\n {matches.length === 0 ? (\n <span style={{ ...itemStyle, color: MUTED, cursor: 'default' }}>\n {choices.length === 0\n ? (zh ? '没有可选模型,直接输入 id 后回车' : 'no models to pick from — type an id and press Enter')\n : (zh ? '没有匹配的模型' : 'no matching model')}\n </span>\n ) : matches.map((route, index) => (\n <button\n key={route}\n type=\"button\"\n role=\"option\"\n aria-selected={index === highlight}\n style={{ ...itemStyle, background: index === highlight ? HOVER : 'transparent' }}\n onMouseEnter={() => setHighlight(index)}\n onClick={() => apply(route)}\n >{textOf(route)}</button>\n ))}\n </span>\n ) : null}\n </span>\n )\n}\n","/**\n * The approval ledger as a full conversation tab.\n *\n * This is the \"look at everything that was reviewed\" surface: one row per\n * approval request with the action, the verdict, the reviewer's full rationale,\n * the safer alternative it suggested, the risk grade, which rule routed it, the\n * reviewer route and timing, and the expandable arguments. The header card is the\n * at-a-glance control; this tab is the audit record.\n *\n * It reads the same `approvalReview` projection as the card, so the two can never\n * disagree, and it holds no state of its own.\n * @module dsh-approval-review/client/LedgerView\n */\n\nimport { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'\nimport type { ClientAuditRecord, ClientAuditView, ClientRisk } from './types.ts'\nimport { resetScrollableAncestorToTop } from './scroll.ts'\nimport { ModelPicker } from './ModelPicker.tsx'\n\n/** Props for the ledger tab. */\nexport interface LedgerViewProps {\n /** The session's audit ledger, or undefined before the first frame lands. */\n readonly view: ClientAuditView | undefined\n /** Whether to render copy in Chinese. */\n readonly zh: boolean\n /**\n * Runs one slash command line in this session. A returned string is a failure\n * line the host refused, which the tab surfaces instead of swallowing.\n */\n readonly runCommand?: (line: string) => void | Promise<string | null>\n /**\n * Reviewer routes this deployment offers, as `provider/model`, in pick order.\n * Empty means this host publishes no model list, and the picker falls back to\n * a free-text id.\n */\n readonly modelChoices?: readonly string[]\n /**\n * Loads the locally configured routes on first use, with their display names.\n * Called when the picker opens, so the catalog is only fetched when someone\n * actually picks a model.\n */\n readonly loadModelRoutes?: () => Promise<{\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n }>\n}\n\nconst TEXT = 'var(--dsw-alias-label-primary, #e6edf3)'\nconst MUTED = 'var(--dsw-alias-label-tertiary, #8b949e)'\nconst BORDER = 'var(--dsw-alias-border-l2, #30363d)'\nconst PANEL = 'var(--dsw-alias-bg-layer-2, #161b22)'\nconst ROW = 'var(--dsw-alias-bg-layer-1, #0d1117)'\nconst ALLOWED = 'var(--dsw-alias-state-success-primary, #2ea043)'\nconst REFUSED = 'var(--dsw-alias-state-error-primary, #f85149)'\nconst WARN = 'var(--dsw-alias-state-warn-primary, #d29922)'\nconst CODE = 'var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)'\n\n/** Risk tone; unknown risk is neutral rather than reassuring. */\nfunction riskTone(risk: ClientRisk | undefined): string {\n if (risk === 'low') return ALLOWED\n if (risk === 'medium') return WARN\n if (risk === 'high' || risk === 'critical') return REFUSED\n return MUTED\n}\n\n/**\n * Whether the plugin was even responsible for one row, from the policy that\n * routed it. A `human` or `never` row sits on the card because the user asked\n * for a record of every approval, NOT because a reviewer judged it.\n */\nfunction routingTag(record: ClientAuditRecord, zh: boolean): string | undefined {\n if (record.policy === 'never') return zh ? '硬禁用' : 'hard-disabled'\n if (record.policy === 'human') return zh ? '交还人工' : 'delegated'\n return undefined\n}\n\n/**\n * The rationale line, told truthfully.\n *\n * A missing rationale has three very different causes and the row must not\n * blame the wrong one: a `never` row never ran a reviewer, a `human` row was\n * handed back to the human answerer, and an `ai` row either never reached the\n * reviewer or completed with the allow rationale left unpersisted\n * (`recordAllowedVerdicts: false`, or a value-projection accept).\n */\nfunction rationaleText(record: ClientAuditRecord, zh: boolean): string {\n if (record.reason !== undefined) return record.reason\n if (record.policy === 'never') {\n return zh\n ? '按 never 策略硬禁用,没有经过复核模型。'\n : 'Hard-disabled by the never policy; no reviewer ran.'\n }\n if (record.policy === 'human') {\n return zh\n ? '已交还人工应答者,本插件没有裁决这一次。'\n : 'Delegated to the human answerer; this plugin did not decide it.'\n }\n return record.refused\n ? (zh ? '被否决,但本行没有留下理由记录。' : 'Refused, but no rationale was recorded.')\n : (zh\n ? '已放行;本行没有留下理由记录(该请求未走到复核模型,或核可理由未落盘)。'\n : 'Allowed, but no rationale was recorded (the request never reached the reviewer, or its allow rationale was not persisted).')\n}\n\n\n/** Short wall-clock stamp. */\nfunction stamp(epochMs: number): string {\n const d = new Date(epochMs)\n const p = (n: number): string => String(n).padStart(2, '0')\n return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n}\n\n/** A labelled field row. */\nfunction Field({ label, children, mono }: {\n label: string\n children: React.ReactNode\n mono?: boolean\n}): React.JSX.Element {\n return (\n <div style={{ display: 'flex', gap: 10, alignItems: 'baseline' }}>\n <span style={{ color: MUTED, flex: '0 0 auto', width: 76, fontSize: 11 }}>{label}</span>\n <span style={{\n color: TEXT,\n fontSize: mono === true ? 11 : 12,\n fontFamily: mono === true ? CODE : undefined,\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n flex: '1 1 auto',\n }}>{children}</span>\n </div>\n )\n}\n\n/** Merge the base list with the loaded catalog, keeping the base order first. */\nfunction reviewerRoutesMerge(base: readonly string[], loaded: readonly string[]): readonly string[] {\n const out = [...base]\n for (const route of loaded) if (!out.includes(route)) out.push(route)\n return out\n}\n\n/** One ledger entry, expanded. */\nfunction Entry({ record, zh, onApprove, deniedIndex }: {\n record: ClientAuditRecord\n zh: boolean\n onApprove?: (record: ClientAuditRecord, denialIndex: number) => void\n deniedIndex: number\n}): React.JSX.Element {\n const [showArgs, setShowArgs] = useState(false)\n const pending = record.outcome === undefined\n const verdict = pending\n ? (zh ? '进行中' : 'pending')\n : record.refused\n ? (zh ? '否决' : 'refused')\n : record.outcome === 'allowed-once' ? (zh ? '放行' : 'allowed') : (zh ? '转人工' : 'delegated')\n const tone = pending ? MUTED : record.refused ? REFUSED : ALLOWED\n\n return (\n <div style={{\n border: `1px solid ${BORDER}`,\n borderLeft: `3px solid ${tone}`,\n borderRadius: 8,\n background: ROW,\n padding: '12px 14px',\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n }}>\n <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>\n <span style={{ fontFamily: CODE, fontSize: 13, fontWeight: 600, color: TEXT }}>{record.toolName}</span>\n <span style={{ fontSize: 11, color: tone, border: `1px solid ${tone}`, borderRadius: 999, padding: '1px 7px' }}>{verdict}</span>\n {routingTag(record, zh) === undefined ? null : (\n <span style={{ fontSize: 11, color: MUTED, border: `1px solid ${BORDER}`, borderRadius: 999, padding: '1px 7px' }}>\n {routingTag(record, zh)}\n </span>\n )}\n {record.risk === undefined ? null : (\n <span style={{ fontSize: 11, color: riskTone(record.risk) }}>{zh ? '风险' : 'risk'} {record.risk}</span>\n )}\n {record.overridden ? <span style={{ fontSize: 11, color: WARN }}>{zh ? '含人工一次性授权' : 'human override'}</span> : null}\n <span style={{ marginLeft: 'auto', fontSize: 11, color: MUTED }}>\n {stamp(record.startedAt)} · T{record.turn}/S{record.step}\n {record.durationMs === undefined ? '' : ` · ${record.durationMs} ms`}\n </span>\n </div>\n\n <Field label={zh ? '裁决理由' : 'rationale'}>\n <span style={{ color: record.reason === undefined ? MUTED : TEXT }}>{rationaleText(record, zh)}</span>\n </Field>\n\n {record.suggestion === undefined ? null : (\n <Field label={zh ? '更安全的做法' : 'safer path'}>{record.suggestion}</Field>\n )}\n <Field label={zh ? '路由策略' : 'routing'} mono>{record.policy} · {record.policySource}</Field>\n {record.askReason === undefined ? null : (\n <Field label={zh ? '申请理由' : 'asked why'}>{record.askReason}</Field>\n )}\n {record.reviewerRoute === undefined ? null : (\n <Field label={zh ? '复核模型' : 'reviewer'} mono>\n {record.reviewerRoute}{record.uncertain ? ` · ${zh ? '不确定' : 'uncertain'}` : ''}\n </Field>\n )}\n\n {record.argumentsPreview === undefined ? null : (\n <div>\n <button\n type=\"button\"\n onClick={() => setShowArgs(v => !v)}\n style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', color: 'var(--dsw-alias-link, #58a6ff)', fontSize: 11 }}\n >{showArgs ? (zh ? '收起参数' : 'hide arguments') : (zh ? '查看参数' : 'show arguments')}</button>\n {showArgs ? (\n <pre style={{\n margin: '6px 0 0', padding: 10, borderRadius: 6, background: PANEL,\n fontFamily: CODE, fontSize: 11, color: TEXT, whiteSpace: 'pre-wrap',\n wordBreak: 'break-word', maxHeight: 320, overflow: 'auto',\n }}>{record.argumentsPreview}</pre>\n ) : null}\n </div>\n )}\n\n {record.refused && record.outcome === 'rejected' && onApprove !== undefined ? (\n <button\n type=\"button\"\n onClick={() => onApprove(record, deniedIndex)}\n style={{\n alignSelf: 'flex-start', cursor: 'pointer', fontSize: 11, padding: '3px 9px',\n borderRadius: 6, border: `1px solid ${BORDER}`, background: 'transparent', color: TEXT,\n }}\n >{zh ? `授权重试第 ${deniedIndex} 条否决` : `approve denial #${deniedIndex} for one retry`}</button>\n ) : null}\n </div>\n )\n}\n\n/** The full ledger tab. */\n/**\n * Start a freshly opened ledger at its top.\n *\n * The tab renders inside the conversation's resident scrollport, which the\n * transcript keeps pinned to its newest line, so a ledger mounted under it would\n * show its own BOTTOM — while the ledger lists the newest decision FIRST. The\n * walk that finds the box to reset lives in `./scroll.ts`.\n * @param root - the ledger's root element.\n */\nfunction useStartAtTop(root: React.RefObject<HTMLDivElement | null>): void {\n useEffect(() => {\n const element = root.current\n if (element === null) return\n // Our own container first: it is the scroller whenever the height chain\n // reaches it, and a fresh mount already starts at zero there.\n element.scrollTop = 0\n if (typeof window === 'undefined') return\n resetScrollableAncestorToTop(element, node => window.getComputedStyle(node).overflowY)\n }, [root])\n}\n\nexport function LedgerView({ view, zh, runCommand, modelChoices, loadModelRoutes }: LedgerViewProps): React.JSX.Element {\n const rootRef = useRef<HTMLDivElement>(null)\n useStartAtTop(rootRef)\n const [loadedChoices, setLoadedChoices] = useState<readonly string[] | undefined>(undefined)\n /** Route id → the catalog's display name, when it differs from the id. */\n const [modelLabels, setModelLabels] = useState<Readonly<Record<string, string>>>({})\n // A refused command used to look identical to a click that did nothing.\n const [commandError, setCommandError] = useState<string | null>(null)\n const run = (line: string): void => {\n if (runCommand === undefined) return\n setCommandError(null)\n void Promise.resolve(runCommand(line))\n .then((failure) => { if (typeof failure === 'string') setCommandError(failure) })\n .catch((error: unknown) => { setCommandError(String(error)) })\n }\n // The base list (override in force + session model) paints immediately; the\n // catalog replaces it once loaded, so the picker is never empty in between.\n const choices = loadedChoices ?? modelChoices ?? []\n const records = view?.records ?? []\n const denials = useMemo(() => records.filter(r => r.refused), [records])\n const reviewedCount = useMemo(() => records.filter(r => r.policy === 'ai').length, [records])\n const deniedIndexOf = (record: ClientAuditRecord): number =>\n denials.findIndex(d => d.reviewId === record.reviewId) + 1\n\n const headerStyle: CSSProperties = {\n display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',\n padding: '0 2px 10px', borderBottom: `1px solid ${BORDER}`, marginBottom: 12,\n }\n\n return (\n <div ref={rootRef} style={{ padding: '14px 16px', overflow: 'auto', height: '100%', fontFamily: 'inherit' }}>\n <div style={headerStyle}>\n <strong style={{ fontSize: 14, color: TEXT }}>{zh ? '审批审计' : 'Approval audit'}</strong>\n <span style={{ fontSize: 12, color: view?.enabled === false ? MUTED : ALLOWED }}>\n {view === undefined ? (zh ? '尚无数据' : 'no data') : view.enabled ? (zh ? '自动审批已开启' : 'auto-approval on') : (zh ? '自动审批已关闭' : 'auto-approval off')}\n </span>\n {runCommand === undefined ? null : (\n <>\n <button type=\"button\" disabled={view?.enabled === true} onClick={() => run('/approval-review on')}\n style={{ fontSize: 11, padding: '3px 9px', borderRadius: 6, cursor: view?.enabled === true ? 'default' : 'pointer', border: `1px solid ${BORDER}`, color: view?.enabled === true ? MUTED : TEXT, background: 'transparent' }}>\n {zh ? '开启' : 'on'}\n </button>\n <button type=\"button\" disabled={view?.enabled === false} onClick={() => run('/approval-review off')}\n style={{ fontSize: 11, padding: '3px 9px', borderRadius: 6, cursor: view?.enabled === false ? 'default' : 'pointer', border: `1px solid ${BORDER}`, color: view?.enabled === false ? MUTED : TEXT, background: 'transparent' }}>\n {zh ? '关闭' : 'off'}\n </button>\n </>\n )}\n {view === undefined ? null : (\n <span style={{ fontSize: 11, color: MUTED, fontFamily: CODE }}>\n {zh ? '复核模型 ' : 'reviewer '}\n {view.reviewerModel.length > 0\n ? `${view.reviewerProvider.length > 0 ? `${view.reviewerProvider}/` : ''}${view.reviewerModel}`\n : (zh ? '继承会话' : 'inherit session')}\n </span>\n )}\n {runCommand === undefined ? null : (\n <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>\n <ModelPicker\n choices={choices}\n labels={modelLabels}\n runCommand={run}\n loadModelRoutes={loadModelRoutes}\n zh={zh}\n onRoutesLoaded={({ routes, labels }) => {\n setModelLabels(labels)\n setLoadedChoices(routes.length === 0 ? (modelChoices ?? []) : reviewerRoutesMerge(modelChoices ?? [], routes))\n }}\n />\n <button\n type=\"button\"\n onClick={() => run('/approval-review model default')}\n style={{ fontSize: 11, padding: '3px 9px', borderRadius: 6, cursor: 'pointer', border: `1px solid ${BORDER}`, color: TEXT, background: 'transparent' }}\n >{zh ? '继承' : 'inherit'}</button>\n </span>\n )}\n {view === undefined || view.total === 0 ? null : (\n <span style={{ fontSize: 11, color: MUTED }}>\n {zh\n ? `共 ${view.total} 次 · 本插件裁决 ${reviewedCount} · 已否决 ${view.refused} · 本回合复审 ${view.reviewsThisTurn}/${view.maxReviewsPerTurn} · 连续否决 ${view.consecutiveDenials}`\n : `${view.total} total · ${reviewedCount} routed to the reviewer · ${view.refused} refused · this turn ${view.reviewsThisTurn}/${view.maxReviewsPerTurn} · streak ${view.consecutiveDenials}`}\n </span>\n )}\n </div>\n\n {commandError === null ? null : (\n <div style={{ fontSize: 11, color: REFUSED, marginBottom: 8 }}>\n {zh ? '命令被拒:' : 'command refused: '}{commandError}\n </div>\n )}\n\n {view?.circuitOpen === true ? (\n <div style={{ fontSize: 12, color: REFUSED, marginBottom: 10 }}>\n {zh ? '否决熔断已触发:本回合后续请求转人工审批。' : 'Rejection breaker is open: later requests in this turn go to the human chain.'}\n </div>\n ) : null}\n\n {records.length === 0 ? (\n <div style={{ fontSize: 13, color: MUTED, padding: '24px 4px', lineHeight: 1.7 }}>\n {zh\n ? '本会话还没有审批记录。当某个动作需要越过沙箱边界时,这里会留下完整的裁决理由、风险等级与更安全的替代做法。'\n : 'No approvals recorded in this session yet. When an action needs to cross the sandbox boundary, its full rationale, risk grade, and safer alternative land here.'}\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>\n {records.map(record => (\n <Entry\n key={record.reviewId}\n record={record}\n zh={zh}\n deniedIndex={deniedIndexOf(record)}\n onApprove={runCommand === undefined\n ? undefined\n : (_r, index) => run(`/approval-review approve ${index}`)}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n","/**\n * The \"approve for me\" access-mode glyph, installed from this plugin.\n *\n * Background: the composer's access-mode menu draws a shield glyph for each\n * permission preset, and that glyph table is a CLOSED design set inside\n * `@deepseek-ai/dsh-client-ui-conversation` — a preset key outside it renders\n * with no icon at all, and the host cannot be asked for one (the `permissions`\n * projection carries value/name/description only). So a plugin that adds a\n * fourth preset gets a fourth menu row with no picture next to it.\n *\n * Two ways out exist. Editing the harness package is the tidy one, but it only\n * takes effect after that package is rebuilt, and it couples the harness to a\n * plugin-specific key. This module is the other one: the plugin decorates the\n * two buttons the menu renders, from the outside, with the same shield+eye mark.\n *\n * Why it decorates by ATTRIBUTE and not by inserting nodes: these buttons belong\n * to React. Inserting a child would put an unknown node where React expects its\n * own child list and would make a later re-render reconcile against DOM it never\n * produced. Setting a `data-*` attribute and drawing the glyph from a\n * plugin-owned stylesheet via `::before` leaves React's tree untouched — React\n * does not remove attributes it never set, and a remount simply loses the mark\n * until the next pass, which the observer re-applies.\n *\n * The shim YIELDS to the built-in glyph: when the harness glyph table already\n * covers this key (i.e. the package was rebuilt with it), the mark is removed\n * and the stylesheet draws nothing, so a rebuild never produces a double icon.\n *\n * Naming coupling: the preset key comes from `Config.reviewerPreset` and the\n * bundle patch's `permission.presets` entry, and the display name from that same\n * entry's `name`. The DOM exposes the NAME (the trigger's `aria-label`, a menu\n * row's text), never the key, so the label list below is what this shim matches.\n * Rename the preset and the glyph simply does not appear; the menu keeps\n * working, which is why this is a progressive enhancement and not a dependency.\n * @module dsh-approval-review/client/access-mode-glyph\n */\n\n/**\n * Display names the preset may carry, matched against the access-mode trigger's\n * `aria-label` (\"访问模式,当前:替我审批\" / \"Access mode, current: Approve for\n * me\") and against a menu row's own text. The bundle patch ships the Chinese\n * name; the English form is accepted so an English deployment still gets its\n * glyph.\n */\nconst PRESET_LABELS: readonly string[] = ['替我审批', 'Approve for me']\n\n/** Marks a decorated button; also the selector the stylesheet hangs off. */\nconst MARK_ATTRIBUTE = 'data-dsh-approval-review-glyph'\n\n/** The style element's identity, so a re-install replaces its own node. */\nconst STYLE_ATTRIBUTE = 'data-dsh-approval-review-glyph-style'\n\n/** The shield outline shared by every built-in access-mode glyph. */\nconst SHIELD_OUTLINE =\n 'M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z'\n\n/**\n * The glyph itself: the same shield as the built-in modes — the boundary is\n * unchanged — carrying an eye, because the reviewer looks at the action before\n * it crosses. Rendered as a MASK, so the mark takes `currentColor` from the\n * button exactly like the built-in `currentColor` SVGs do.\n */\nconst GLYPH_SVG = '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"none\">'\n + `<path d=\"${SHIELD_OUTLINE}\" stroke=\"#000\" stroke-width=\"1.31831\" stroke-linejoin=\"round\"/>`\n + '<path d=\"M5.348 6.58C6.224 7.773 7.029 8.375 8.2 8.375C9.371 8.375 10.176 7.773 11.052 6.58\" stroke=\"#000\" stroke-width=\"1.31831\" stroke-linecap=\"round\"/>'\n + '<path d=\"M5.348 6.58C6.224 5.387 7.029 4.785 8.2 4.785C9.371 4.785 10.176 5.387 11.052 6.58\" stroke=\"#000\" stroke-width=\"1.31831\" stroke-linecap=\"round\"/>'\n + '<circle cx=\"8.2\" cy=\"6.58\" r=\"0.95\" fill=\"#000\"/>'\n + '</svg>'\n\n/**\n * The one selector every pass and every mutation check uses.\n *\n * It is deliberately narrow: the trigger is addressed by SUBSTRING on the\n * `aria-label` (a native attribute test, not a JavaScript scan of every button\n * in the document), menu rows by role, and already-marked buttons — whichever\n * they are — by the mark itself, so a mode switch clears the old one.\n */\nconst TARGET_SELECTOR = [\n ...PRESET_LABELS.map(label => `button[aria-label*=\"${label}\"]`),\n 'button[role=\"menuitem\"]',\n `[${MARK_ATTRIBUTE}]`,\n].join(', ')\n\n/** Build the stylesheet that draws the mark on a decorated button. */\nfunction stylesheet(): string {\n const mask = `url(\"data:image/svg+xml,${encodeURIComponent(GLYPH_SVG)}\")`\n return `\n[${MARK_ATTRIBUTE}]::before{\n content:\"\";\n display:inline-block;\n flex:none;\n width:16px;\n height:16px;\n background-color:currentColor;\n -webkit-mask-image:${mask};\n mask-image:${mask};\n -webkit-mask-repeat:no-repeat;\n mask-repeat:no-repeat;\n -webkit-mask-position:center;\n mask-position:center;\n -webkit-mask-size:contain;\n mask-size:contain;\n}\n/* The composer trigger sizes its icons at 14px. */\nbutton[aria-label][${MARK_ATTRIBUTE}]::before{\n width:14px;\n height:14px;\n}\n`\n}\n\n/**\n * Whether an element already carries the harness's own glyph.\n *\n * Both surfaces render the built-in icon as a leading `<span>` holding an\n * `<svg>`; the trailing chevron is a later sibling, so only the first child\n * counts. When this is true the shim steps aside.\n */\nfunction hasBuiltInGlyph(button: HTMLElement): boolean {\n const first = button.firstElementChild\n return first instanceof HTMLElement\n && first.tagName === 'SPAN'\n && first.querySelector('svg') !== null\n}\n\n/**\n * The pure matching rule behind the shim.\n *\n * Extracted from the DOM pass so the rule that can actually go wrong — which\n * button is this plugin's preset, and does it already have a built-in glyph —\n * is testable without a browser. The DOM *plumbing* around it is decoration and\n * degrades to \"no glyph\", never to a broken menu.\n * @param facts - the element facts the shim matches on.\n * @returns `'mark'` when this plugin should draw the glyph, `'skip'` otherwise.\n */\nexport function accessModeGlyphDecision(facts: {\n readonly ariaLabel?: string | null\n readonly role?: string | null\n readonly text?: string\n readonly hasBuiltInGlyph?: boolean\n}): 'mark' | 'skip' {\n const label = facts.ariaLabel ?? ''\n const isTarget = PRESET_LABELS.some(candidate => label.includes(candidate))\n || (facts.role === 'menuitem' && PRESET_LABELS.includes((facts.text ?? '').trim()))\n if (!isTarget) return 'skip'\n return facts.hasBuiltInGlyph === true ? 'skip' : 'mark'\n}\n\n/**\n * Install the access-mode glyph decoration.\n *\n * The observer is deliberately cheap: it inspects only MUTATED subtrees for the\n * target selector and coalesces every hit into one animation frame, so a\n * streaming conversation (which appends text nodes constantly) never becomes a\n * per-token query over the document.\n * @param root - document to decorate; injectable for tests.\n * @returns the disposer that removes the stylesheet, the marks, and the observer.\n */\nexport function installAccessModeGlyph(root: Document = document): () => void {\n if (root.querySelector(`style[${STYLE_ATTRIBUTE}]`) === null) {\n const style = root.createElement('style')\n style.setAttribute(STYLE_ATTRIBUTE, '1')\n style.textContent = stylesheet()\n root.head.appendChild(style)\n }\n\n const decorate = (): void => {\n for (const button of root.querySelectorAll<HTMLElement>(TARGET_SELECTOR)) {\n const decision = accessModeGlyphDecision({\n ariaLabel: button.getAttribute('aria-label'),\n role: button.getAttribute('role'),\n text: button.textContent ?? '',\n hasBuiltInGlyph: hasBuiltInGlyph(button),\n })\n if (decision === 'mark') {\n if (!button.hasAttribute(MARK_ATTRIBUTE)) button.setAttribute(MARK_ATTRIBUTE, '1')\n } else if (button.hasAttribute(MARK_ATTRIBUTE)) {\n // Either the harness grew its own glyph, or the access mode moved on.\n // Both mean this plugin's mark no longer belongs on this button.\n button.removeAttribute(MARK_ATTRIBUTE)\n }\n }\n }\n\n /** True when a mutation could have produced one of the decorated buttons. */\n const mightMatter = (node: Node): boolean => {\n if (!(node instanceof Element)) return false\n if (node.matches(TARGET_SELECTOR)) return true\n return node.querySelector(TARGET_SELECTOR) !== null\n }\n\n let frame: number | undefined\n const schedule = (): void => {\n if (frame !== undefined) return\n frame = root.defaultView?.requestAnimationFrame(() => {\n frame = undefined\n decorate()\n })\n }\n\n const observer = new MutationObserver(records => {\n for (const record of records) {\n // A change INSIDE a target button matters too: React growing the built-in\n // icon inserts a span, not a button, and this shim has to notice that and\n // step aside — otherwise a harness rebuild paints two glyphs.\n const target = record.target\n if (target instanceof Element && target.closest(TARGET_SELECTOR) !== null) {\n schedule()\n return\n }\n for (const node of record.addedNodes) {\n if (mightMatter(node)) {\n schedule()\n return\n }\n }\n }\n })\n // A client bundle can execute before `body` exists; the initial pass still\n // runs, and the observer simply arrives with the next install.\n if (root.body !== null) observer.observe(root.body, { childList: true, subtree: true })\n\n // The initial pass covers a composer that rendered before this plugin mounted.\n decorate()\n\n return () => {\n observer.disconnect()\n if (frame !== undefined) root.defaultView?.cancelAnimationFrame(frame)\n for (const button of root.querySelectorAll<HTMLElement>(`[${MARK_ATTRIBUTE}]`)) {\n button.removeAttribute(MARK_ATTRIBUTE)\n }\n root.querySelector(`style[${STYLE_ATTRIBUTE}]`)?.remove()\n }\n}\n","/**\n * One slash-command line sent from this plugin's browser half to the host.\n *\n * The remote facade's arity is the whole point of this module. The host\n * declares\n *\n * ```ts\n * // packages/interaction/commands/src/index.ts\n * @Remote\n * async execute(agent, line, submittedAttachments, signal)\n * ```\n *\n * — three business arguments, because a command may carry submitted\n * attachments. Calling it with two does not fail at compile time against a\n * HAND-WRITTEN structural type (this plugin declares its own), it fails at\n * runtime with `commands/execute expected 3 business argument(s) … got 2`, and\n * every button in the tab becomes a silent no-op. That is exactly what happened,\n * so the call and its error mapping live here with tests.\n * @module dsh-approval-review/client/run-command\n */\n\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\n\n/** One command outcome as the remote facade reports it. */\nexport interface CommandRemoteResult {\n /** The handler's settled result, or undefined when the line did not resolve. */\n readonly value?: {\n readonly result: { readonly kind?: string; readonly text?: string }\n } | undefined\n}\n\n/** The command remote face this plugin drives. */\nexport interface CommandRemoteFace {\n readonly commands: {\n /**\n * Execute one command line in a session.\n * @param sessionId - the session the command runs in.\n * @param line - the full line, leading slash included.\n * @param attachments - submitted attachments; always `[]` for this plugin.\n * @returns the remote wrapper: `ok` discriminates refusal, `value` absence\n * means the host never resolved the line.\n */\n execute(\n sessionId: SessionId,\n line: string,\n attachments: readonly unknown[],\n ): Promise<\n | { readonly ok: true; readonly value: CommandRemoteResult['value'] }\n | { readonly ok: false; readonly error: { readonly code: string; readonly message: string } }\n >\n }\n}\n\n/**\n * Send one command line and reduce the outcome to \"failure text, or null\".\n *\n * A handler that answered `kind: 'error'` is a REFUSAL the operator must see\n * (`/approval-review nonsense` says so), while an unresolved line means the host\n * does not know the command at all. Both are returned as text, never thrown, so\n * the tab can show them next to the control that failed.\n * @param remote - the command remote, when the client has mounted it.\n * @param sessionId - the session the command runs in.\n * @param line - the full command line.\n * @returns null on success; a human-readable failure line otherwise.\n */\nexport async function runCommandLine(\n remote: CommandRemoteFace['commands'] | undefined,\n sessionId: SessionId,\n line: string,\n): Promise<string | null> {\n if (remote === undefined) return 'the command remote is not mounted in this client'\n try {\n const result = await remote.execute(sessionId, line, [])\n if (!result.ok) return `${result.error.message} (${result.error.code})`\n if (result.value === undefined) return `the host did not resolve \"${line}\"`\n const text = result.value.result.text\n return result.value.result.kind === 'error'\n ? (text !== undefined && text.length > 0 ? text : `\"${line}\" was refused`)\n : null\n } catch (error: unknown) {\n return `\"${line}\" failed: ${String(error)}`\n }\n}\n","/**\n * Browser half of the automatic-approval control surface.\n *\n * Contributes:\n * 1. a conversation tab — the approval ledger as a full page, beside 轨迹 /\n * 上下文 / 费用: every request, the reviewer's verdict and rationale, the\n * routing policy, risk, route, timing, and the live counters;\n * 2. the access-mode glyph for this plugin's `approve-for-me` preset.\n *\n * The session-header card this plugin used to contribute is gone: it duplicated\n * the tab's ledger in a popover that the tab already renders full-page, and the\n * header is the most contended strip of the session chrome.\n *\n * It also decorates the access-mode control with the glyph for this plugin's\n * `approve-for-me` preset, which the harness's closed glyph table cannot know\n * about (see `./access-mode-glyph.ts`).\n *\n * The host provides data through the `approvalReview` session projection, so\n * this half reads only whole projection values. Both surfaces drive the host\n * through the slash command — the same path a human typing it would take — which\n * keeps this half free of any assumption about the host's internal services.\n * @module dsh-approval-review/client\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\n// Type-only: the slot-scope augmentation that types `ctx.slots`, the session\n// standard props (`useProjection`), and the composer dock slot name.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { LedgerView } from './LedgerView.tsx'\nimport { installAccessModeGlyph } from './access-mode-glyph.ts'\nimport { directoryRoutes, reviewerRouteChoices } from './model-choices.ts'\nimport { runCommandLine, type CommandRemoteFace } from './run-command.ts'\nimport type { ClientAuditView } from './types.ts'\n\n/** Slot entry id; stable so a redeploy replaces its own row. */\nexport const VIEW_SLOT_ID = 'approval-review-ledger'\n\n/** The conversation tab strip, beside 轨迹 / 上下文 / 费用. */\nconst VIEW_SLOT = 'conversation.view'\n\n/**\n * Required client services.\n *\n * `remote.commands` is a named remote SERVICE, not a plain property: the client\n * remote facade throws `cannot get property \"remote.commands\" without inject`\n * unless the key is declared here. Declaring `remote` alone is not enough, which\n * is exactly the bug this cost once.\n */\nexport const inject = ['slots', 'remote', 'remote.commands']\n\n/**\n * The client model directory this harness publishes for model picking.\n *\n * Read structurally and OPTIONALLY: it is what the composer's model seat and the\n * `/model` picker use, so it is the deployment's own answer to \"which models are\n * configured here\". `subagentModelSelectionPolicy` would be the tighter list,\n * but it is host-only (no `wire`), so it never reaches the browser.\n */\ninterface ModelDirectoriesFace {\n /**\n * @param sessionId - the session whose catalog to load.\n * @returns the loaded directory snapshot.\n */\n directoryFor(sessionId: SessionId): {\n load(): Promise<{ readonly groups?: unknown }>\n }\n}\n\n/** Props the framework supplies to a session-scoped slot entry. */\ninterface SessionActionProps {\n /** Host-computed projection values addressed by key. */\n readonly useProjection: (key: string) => unknown\n /** Current session identity, absent while no session is selected. */\n readonly sessionId?: SessionId\n}\n\n/** Whether copy should be Chinese, from the browser language. */\nfunction preferZh(): boolean {\n return typeof navigator !== 'undefined' && navigator.language.toLowerCase().startsWith('zh')\n}\n\n/**\n * The business face the slot framework injects per session. Carrying the session\n * id here — rather than in module state — is what makes the control correct for\n * whichever session it is rendered in.\n */\nexport interface ApprovalReviewInjected {\n /**\n * Load the locally configured reviewer routes with their display names.\n * @returns route ids plus a route→display-name map.\n */\n loadModelRoutes: () => Promise<{ readonly routes: readonly string[]; readonly labels: Readonly<Record<string, string>> }>\n /**\n * Execute one approval-review command line in this session.\n * @param line - the full command line, leading slash included.\n * @returns null when the host admitted it; a failure line otherwise.\n */\n runCommand: (line: string) => Promise<string | null>\n}\n\n/** Reviewer routes this deployment offers, read from its own projections. */\nfunction reviewerChoices(props: SessionActionProps, current: string | undefined): readonly string[] {\n return reviewerRouteChoices({\n current,\n sessionDefault: props.useProjection('modelSelection'),\n allowed: props.useProjection('subagentModelSelectionPolicy'),\n })\n}\n\n/** Render the full ledger tab. */\nfunction ApprovalReviewLedger(props: SessionActionProps & ApprovalReviewInjected): React.JSX.Element {\n const view = props.useProjection('approvalReview') as ClientAuditView | undefined\n const current = view === undefined || view.reviewerModel.length === 0\n ? undefined\n : `${view.reviewerProvider.length > 0 ? `${view.reviewerProvider}/` : ''}${view.reviewerModel}`\n return LedgerView({\n view,\n zh: preferZh(),\n runCommand: props.runCommand,\n modelChoices: reviewerChoices(props, current),\n loadModelRoutes: props.loadModelRoutes,\n })\n}\n\n/**\n * Register the ledger tab and the access-mode glyph.\n * @param ctx - client Cordis context.\n */\nexport function apply(ctx: ClientContext): void {\n // The glyph is decoration over a control this plugin's preset is part of; it\n // owns a style element, a few attributes, and one observer, all released by\n // the effect when the plugin unmounts or reloads.\n ctx.effect(() => installAccessModeGlyph(), 'approval-review: access-mode glyph')\n /**\n * The remote is resolved LAZILY, per call. Capturing `ctx.remote` in the\n * `apply` closure is wrong: `apply` can run before the remote facade finishes\n * mounting, and a captured `undefined` turns every click into a silent no-op\n * on a control that still LOOKS enabled. That was a real bug here.\n */\n const remoteOf = (): CommandRemoteFace['commands'] | undefined =>\n (ctx as unknown as { remote?: CommandRemoteFace }).remote?.commands\n\n /**\n * The model directory, resolved LAZILY for the same reason the command remote\n * is: this client half mounts before every service it may use is up, and a\n * captured `undefined` would permanently disable the picker.\n */\n const directoriesOf = (): ModelDirectoriesFace | undefined =>\n (ctx as unknown as { get?: (name: string) => unknown }).get?.('modelDirectories') as ModelDirectoriesFace | undefined\n\n /** The per-session business face the tab's seat uses. */\n // The seat hands the session id as a plain string; the command remote takes\n // the branded id, so the brand is reasserted at this one boundary.\n const inject = (rawSessionId: string): ApprovalReviewInjected => ({\n loadModelRoutes: async () => {\n const directories = directoriesOf()\n if (directories === undefined) return { routes: [], labels: {} }\n try {\n // The catalog load is shared and cached by the harness, so opening the\n // picker costs nothing after the composer's own model seat has loaded.\n return directoryRoutes(await directories.directoryFor(rawSessionId as SessionId).load())\n } catch {\n return { routes: [], labels: {} }\n }\n },\n runCommand: async (line: string) => {\n // The remote is resolved per call (see `remoteOf`), and the line goes out\n // through `runCommandLine`, which owns the 3-business-argument arity and\n // the refusal mapping.\n return await runCommandLine(remoteOf(), rawSessionId as SessionId, line)\n },\n })\n\n ctx.slots.inject(VIEW_SLOT, () => ctx.slots.register(\n {\n name: VIEW_SLOT,\n id: VIEW_SLOT_ID,\n // After the built-in 轨迹 tab so the strip keeps its familiar order.\n order: 40,\n label: () => (preferZh() ? '审批' : 'Approvals'),\n inject,\n },\n ApprovalReviewLedger,\n ))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["TEXT","MUTED","BORDER","PANEL","CODE","useState","useRef","useMemo","useState","useRef","useMemo"],"sources":["../src/client/scroll.ts","../src/client/model-choices.ts","../src/client/ModelPicker.tsx","../src/client/LedgerView.tsx","../src/client/access-mode-glyph.ts","../src/client/run-command.ts","../src/client/index.tsx"],"sourcesContent":["/**\n * Scroll positioning for a view rendered inside someone else's scrollport.\n *\n * The Approvals tab is mounted inside the conversation's resident scrollport\n * (`.scrollBody`, `overflow-y: auto`), which the transcript keeps pinned to its\n * newest line. Switching tabs does not reset that box, so a ledger mounted under\n * it opens at its own BOTTOM — the opposite of useful when the newest decision\n * is the first row.\n *\n * A plugin cannot claim that scrollport: an intermediate slot element breaks the\n * height chain a `height: 100%; overflow: auto` root would need to become the\n * scroller itself. Resetting the nearest scrollable ancestor is the fix, and the\n * decision of WHICH ancestor that is lives here so it can be tested without a\n * browser.\n * @module dsh-approval-review/client/scroll\n */\n\n/** The minimal element surface the walk needs. */\nexport interface ScrollableNode {\n /** Parent in the element tree, or null at the root. */\n readonly parentElement: ScrollableNode | null\n /** Full content height. */\n readonly scrollHeight: number\n /** Visible height. */\n readonly clientHeight: number\n /** Current scroll offset; assigned when this node is chosen. */\n scrollTop: number\n}\n\n/** Bound on the ancestor walk, so a pathological tree cannot spin. */\nexport const ANCESTOR_WALK_LIMIT = 12\n\n/**\n * Reset the nearest scrollable ancestor that actually overflows.\n *\n * \"Actually overflows\" matters: an ancestor with `overflow-y: auto` but no\n * overflow cannot be scrolled, so resetting it is a no-op that would hide the\n * real scroller further up.\n * @param from - the element whose ancestry to search (usually the view root).\n * @param overflowYOf - computed `overflow-y` accessor for one node.\n * @param maxDepth - stop after this many ancestors.\n * @returns the node that was reset, or undefined when none qualified.\n */\nexport function resetScrollableAncestorToTop<T extends ScrollableNode>(\n from: T | null,\n overflowYOf: (node: T) => string,\n maxDepth: number = ANCESTOR_WALK_LIMIT,\n): T | undefined {\n let node: ScrollableNode | null = from?.parentElement ?? null\n for (let depth = 0; node !== null && depth < maxDepth; depth += 1, node = node.parentElement) {\n const overflowY = overflowYOf(node as T)\n if (overflowY !== 'auto' && overflowY !== 'scroll') continue\n if (node.scrollHeight <= node.clientHeight) continue\n node.scrollTop = 0\n return node as T\n }\n return undefined\n}\n","/**\n * Reviewer-route choices for the Approvals tab's model picker.\n *\n * The reviewer runs as a subagent, so the routes a deployment actually offers it\n * are already published as session projections — this module just reads them\n * instead of hardcoding a model list that would go stale:\n *\n * - `subagentModelSelectionPolicy`: the deployment's allowed subagent routes\n * (`subagent-model-selection.allowedModels` in `settings.yaml`);\n * - `modelSelection.lastUsed`: the session's own route, i.e. what \"inherit\"\n * resolves to.\n *\n * Everything is read structurally and defensively: a projection this host does\n * not publish, or an entry with a non-string half, contributes nothing rather\n * than breaking the picker.\n * @module dsh-approval-review/client/model-choices\n */\n\n/** One `{provider, model}` route, when both halves are strings. */\nfunction routeOf(value: unknown): string | undefined {\n if (typeof value !== 'object' || value === null) return undefined\n const entry = value as { readonly provider?: unknown; readonly model?: unknown }\n if (typeof entry.provider !== 'string' || typeof entry.model !== 'string') return undefined\n if (entry.provider.length === 0 || entry.model.length === 0) return undefined\n return `${entry.provider}/${entry.model}`\n}\n\n/** Every valid route in a projection that carries a list of them. */\nfunction routesOf(value: unknown): readonly string[] {\n if (!Array.isArray(value)) return []\n const out: string[] = []\n for (const entry of value) {\n const route = routeOf(entry)\n if (route !== undefined) out.push(route)\n }\n return out\n}\n\n/**\n * Routes AND their display names from a client model-directory snapshot.\n *\n * The route id is what a command needs, but it is NOT the vendor's model name:\n * `deepseek-official/deepseek-flash` is the harness catalog's id whose display\n * name is \"DeepSeek-V41-Flash\", and a deployment's own providers name their\n * routes freely. Showing both is what stops \"that model is not in my\n * subscription list\" from being a mystery.\n * @param value - the directory state returned by `directoryFor(session).load()`.\n * @returns distinct route ids (catalog order) plus their display names.\n */\nexport function directoryRoutes(value: unknown): {\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n} {\n const routes: string[] = []\n const labels: Record<string, string> = {}\n if (typeof value !== 'object' || value === null) return { routes, labels }\n const groups = (value as { readonly groups?: unknown }).groups\n if (!Array.isArray(groups)) return { routes, labels }\n for (const group of groups) {\n if (typeof group !== 'object' || group === null) continue\n const id = (group as { readonly id?: unknown }).id\n const models = (group as { readonly models?: unknown }).models\n if (typeof id !== 'string' || id.length === 0 || !Array.isArray(models)) continue\n for (const model of models) {\n if (typeof model !== 'object' || model === null) continue\n const modelId = (model as { readonly id?: unknown }).id\n if (typeof modelId !== 'string' || modelId.length === 0) continue\n const route = `${id}/${modelId}`\n if (!routes.includes(route)) routes.push(route)\n const name = (model as { readonly name?: unknown }).name\n if (typeof name === 'string' && name.length > 0 && name !== modelId) labels[route] = name\n }\n }\n return { routes, labels }\n}\n\n/**\n * Every route in a client model-directory snapshot (`modelDirectories` service).\n *\n * This is the SAME catalog the composer's model seat and the `/model` picker\n * read, so the reviewer picker offers exactly the models the deployment\n * configures locally — minus anything the catalog failed to load, which it\n * reports separately and which we deliberately do not guess at.\n * @param value - the directory state returned by `directoryFor(session).load()`.\n * @returns distinct `provider/model` labels in catalog order.\n */\nexport function routesFromDirectory(value: unknown): readonly string[] {\n return directoryRoutes(value).routes\n}\n\n/**\n * Build the picker's option list.\n *\n * Order is deliberate: the session override in force first (so a route chosen\n * outside the deployment's list still shows as the current selection), then the\n * session's own model, then the deployment's allowed subagent routes. Duplicates\n * collapse, so a model that is both the session default and an allowed route\n * appears once.\n * @param input - the projections' raw values plus the override in force.\n * @returns distinct `provider/model` labels, in display order.\n */\nexport function reviewerRouteChoices(input: {\n /** The session override in force, as `provider/model`, when one is set. */\n readonly current?: string | undefined\n /** Raw `modelSelection` projection value. */\n readonly sessionDefault?: unknown\n /** Raw `subagentModelSelectionPolicy` projection value. */\n readonly allowed?: unknown\n}): readonly string[] {\n const out: string[] = []\n const push = (route: string | undefined): void => {\n if (route === undefined || route.length === 0 || out.includes(route)) return\n out.push(route)\n }\n push(input.current)\n const session = typeof input.sessionDefault === 'object' && input.sessionDefault !== null\n ? (input.sessionDefault as { readonly lastUsed?: unknown }).lastUsed\n : undefined\n push(routeOf(session))\n for (const route of routesOf(input.allowed)) push(route)\n return out\n}\n\n/**\n * Filter routes for the picker's list.\n *\n * Matching is a case-insensitive substring over the whole `provider/model`\n * label, so typing `luna` and typing `codex/luna` both narrow to the same row.\n * An empty (or whitespace-only) query keeps the whole list.\n * @param routes - candidate labels.\n * @param query - what the operator typed.\n * @returns the matching labels, in input order.\n */\nexport function filterRoutes(routes: readonly string[], query: string): readonly string[] {\n const needle = query.trim().toLowerCase()\n if (needle.length === 0) return routes\n return routes.filter(route => route.toLowerCase().includes(needle))\n}\n","/**\n * The reviewer-model picker.\n *\n * A native `<datalist>` (or `<select>`) popup is drawn by the browser, not the\n * page: its font, weight, and width ignore CSS entirely, which made the list\n * read as a different, much louder control than the tab it sits in. This is the\n * plugin's own listbox instead, styled with the ledger's own type scale, with a\n * free-text field on top so an id the catalog no longer advertises stays\n * reachable (catalog membership is advisory).\n * @module dsh-approval-review/client/ModelPicker\n */\n\nimport { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'\nimport { filterRoutes } from './model-choices.ts'\n\n/** Props for the picker. */\nexport interface ModelPickerProps {\n /** Routes this deployment offers, in display order. */\n readonly choices: readonly string[]\n /**\n * Route id → the catalog's display name (\"DeepSeek-V41-Flash\" for\n * `deepseek-official/deepseek-flash`). The id is what the command needs, but it\n * is not the vendor's model name, and that mismatch reads as a bug.\n */\n readonly labels?: Readonly<Record<string, string>> | undefined\n /** Apply one shell command line (the picker emits `/approval-review model …`). */\n readonly runCommand: (line: string) => void\n /** Load the catalog on first use. */\n readonly loadModelRoutes?: (() => Promise<{\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n }>) | undefined\n /** Whether to render copy in Chinese. */\n readonly zh: boolean\n /** Replaces the base list once the catalog arrives; base first. */\n readonly onRoutesLoaded: (loaded: {\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n }) => void\n}\n\nconst TEXT = 'var(--dsw-alias-label-primary, #e6edf3)'\nconst MUTED = 'var(--dsw-alias-label-tertiary, #8b949e)'\nconst BORDER = 'var(--dsw-alias-border-l2, #30363d)'\nconst PANEL = 'var(--dsw-alias-bg-layer-2, #161b22)'\nconst HOVER = 'var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.08))'\nconst CODE = 'var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)'\n\n/** The picker's field and its plugin-rendered list. */\nexport function ModelPicker({ choices, labels, runCommand, loadModelRoutes, zh, onRoutesLoaded }: ModelPickerProps): React.JSX.Element {\n const [draft, setDraft] = useState('')\n const [open, setOpen] = useState(false)\n const [highlight, setHighlight] = useState(0)\n const rootRef = useRef<HTMLSpanElement>(null)\n const requestedRef = useRef(false)\n\n const matches = useMemo(() => filterRoutes(choices, draft), [choices, draft])\n\n /** Fetch the catalog once, on first interaction. */\n const loadOnce = (): void => {\n if (requestedRef.current || loadModelRoutes === undefined) return\n requestedRef.current = true\n void loadModelRoutes()\n .then(loaded => onRoutesLoaded(loaded))\n .catch(() => onRoutesLoaded({ routes: [], labels: {} }))\n }\n\n useEffect(() => {\n if (!open) return\n const onPointerDown = (event: MouseEvent): void => {\n if (rootRef.current?.contains(event.target as Node) === true) return\n setOpen(false)\n }\n document.addEventListener('mousedown', onPointerDown)\n return () => { document.removeEventListener('mousedown', onPointerDown) }\n }, [open])\n\n const apply = (route: string): void => {\n const value = route.trim()\n if (value.length === 0) return\n runCommand(`/approval-review model ${value}`)\n setDraft('')\n setOpen(false)\n }\n\n const fieldStyle: CSSProperties = {\n fontFamily: CODE,\n fontSize: 11,\n lineHeight: '16px',\n padding: '2px 6px',\n width: 190,\n borderRadius: 6,\n border: `1px solid ${BORDER}`,\n background: 'transparent',\n color: TEXT,\n outline: 'none',\n }\n\n /** One row's text: the id that will be sent, plus the name a human recognizes. */\n const textOf = (route: string): string => {\n const name = labels?.[route]\n return name === undefined ? route : `${route} · ${name}`\n }\n\n const itemStyle: CSSProperties = {\n display: 'block',\n width: '100%',\n textAlign: 'left',\n background: 'transparent',\n border: 'none',\n borderRadius: 4,\n cursor: 'pointer',\n // The ledger's own type scale: this list is chrome, not a headline.\n fontFamily: CODE,\n fontSize: 11,\n fontWeight: 400,\n lineHeight: '16px',\n padding: '3px 8px',\n color: TEXT,\n }\n\n return (\n <span ref={rootRef} style={{ position: 'relative', display: 'inline-flex' }}>\n <input\n value={draft}\n role=\"combobox\"\n aria-expanded={open}\n aria-label={zh ? '复核模型' : 'reviewer model'}\n placeholder={zh ? '选择或输入模型' : 'pick or type a model'}\n style={fieldStyle}\n onFocus={() => { loadOnce(); setOpen(true) }}\n onClick={() => { loadOnce(); setOpen(true) }}\n onChange={(event) => { setDraft(event.target.value); setHighlight(0); setOpen(true) }}\n onKeyDown={(event) => {\n if (event.key === 'Escape') { setOpen(false); return }\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n if (matches.length === 0) return\n setOpen(true)\n setHighlight(current => {\n const next = event.key === 'ArrowDown' ? current + 1 : current - 1\n return (next + matches.length) % matches.length\n })\n return\n }\n if (event.key !== 'Enter') return\n // Enter takes the highlighted suggestion when the list is open, and\n // otherwise applies exactly what was typed.\n const picked = open ? matches[highlight] : undefined\n apply(picked ?? draft)\n }}\n />\n {open ? (\n <span\n role=\"listbox\"\n style={{\n position: 'absolute',\n top: 'calc(100% + 4px)',\n left: 0,\n zIndex: 60,\n minWidth: '100%',\n maxWidth: 320,\n maxHeight: 220,\n overflowY: 'auto',\n background: PANEL,\n border: `1px solid ${BORDER}`,\n borderRadius: 8,\n padding: 4,\n boxShadow: '0 8px 24px rgba(0,0,0,.45)',\n }}\n >\n {matches.length === 0 ? (\n <span style={{ ...itemStyle, color: MUTED, cursor: 'default' }}>\n {choices.length === 0\n ? (zh ? '没有可选模型,直接输入 id 后回车' : 'no models to pick from — type an id and press Enter')\n : (zh ? '没有匹配的模型' : 'no matching model')}\n </span>\n ) : matches.map((route, index) => (\n <button\n key={route}\n type=\"button\"\n role=\"option\"\n aria-selected={index === highlight}\n style={{ ...itemStyle, background: index === highlight ? HOVER : 'transparent' }}\n onMouseEnter={() => setHighlight(index)}\n onClick={() => apply(route)}\n >{textOf(route)}</button>\n ))}\n </span>\n ) : null}\n </span>\n )\n}\n","/**\n * The approval ledger as a full conversation tab.\n *\n * This is the \"look at everything that was reviewed\" surface: one row per\n * approval request with the action, the verdict, the reviewer's full rationale,\n * the safer alternative it suggested, the risk grade, which rule routed it, the\n * reviewer route and timing, and the expandable arguments. The header card is the\n * at-a-glance control; this tab is the audit record.\n *\n * It reads the same `approvalReview` projection as the card, so the two can never\n * disagree, and it holds no state of its own.\n * @module dsh-approval-review/client/LedgerView\n */\n\nimport { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'\nimport type { ClientAuditRecord, ClientAuditView, ClientRisk } from './types.ts'\nimport { resetScrollableAncestorToTop } from './scroll.ts'\nimport { ModelPicker } from './ModelPicker.tsx'\n\n/** Props for the ledger tab. */\nexport interface LedgerViewProps {\n /** The session's audit ledger, or undefined before the first frame lands. */\n readonly view: ClientAuditView | undefined\n /** Whether to render copy in Chinese. */\n readonly zh: boolean\n /**\n * Runs one slash command line in this session. A returned string is a failure\n * line the host refused, which the tab surfaces instead of swallowing.\n */\n readonly runCommand?: (line: string) => void | Promise<string | null>\n /**\n * Reviewer routes this deployment offers, as `provider/model`, in pick order.\n * Empty means this host publishes no model list, and the picker falls back to\n * a free-text id.\n */\n readonly modelChoices?: readonly string[]\n /**\n * Loads the locally configured routes on first use, with their display names.\n * Called when the picker opens, so the catalog is only fetched when someone\n * actually picks a model.\n */\n readonly loadModelRoutes?: () => Promise<{\n readonly routes: readonly string[]\n readonly labels: Readonly<Record<string, string>>\n }>\n}\n\nconst TEXT = 'var(--dsw-alias-label-primary, #e6edf3)'\nconst MUTED = 'var(--dsw-alias-label-tertiary, #8b949e)'\nconst BORDER = 'var(--dsw-alias-border-l2, #30363d)'\nconst PANEL = 'var(--dsw-alias-bg-layer-2, #161b22)'\nconst ROW = 'var(--dsw-alias-bg-layer-1, #0d1117)'\nconst ALLOWED = 'var(--dsw-alias-state-success-primary, #2ea043)'\nconst REFUSED = 'var(--dsw-alias-state-error-primary, #f85149)'\nconst WARN = 'var(--dsw-alias-state-warn-primary, #d29922)'\nconst CODE = 'var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)'\n\n/** Risk tone; unknown risk is neutral rather than reassuring. */\nfunction riskTone(risk: ClientRisk | undefined): string {\n if (risk === 'low') return ALLOWED\n if (risk === 'medium') return WARN\n if (risk === 'high' || risk === 'critical') return REFUSED\n return MUTED\n}\n\n/**\n * Whether the plugin was even responsible for one row, from the policy that\n * routed it. A `human` or `never` row sits on the card because the user asked\n * for a record of every approval, NOT because a reviewer judged it.\n */\nfunction routingTag(record: ClientAuditRecord, zh: boolean): string | undefined {\n if (record.policy === 'never') return zh ? '硬禁用' : 'hard-disabled'\n if (record.policy === 'human') return zh ? '交还人工' : 'delegated'\n return undefined\n}\n\n/**\n * The rationale line, told truthfully.\n *\n * A missing rationale has three very different causes and the row must not\n * blame the wrong one: a `never` row never ran a reviewer, a `human` row was\n * handed back to the human answerer, and an `ai` row either never reached the\n * reviewer or completed with the allow rationale left unpersisted\n * (`recordAllowedVerdicts: false`, or a value-projection accept).\n */\nfunction rationaleText(record: ClientAuditRecord, zh: boolean): string {\n if (record.reason !== undefined) return record.reason\n if (record.policy === 'never') {\n return zh\n ? '按 never 策略硬禁用,没有经过复核模型。'\n : 'Hard-disabled by the never policy; no reviewer ran.'\n }\n if (record.policy === 'human') {\n return zh\n ? '已交还人工应答者,本插件没有裁决这一次。'\n : 'Delegated to the human answerer; this plugin did not decide it.'\n }\n return record.refused\n ? (zh ? '被否决,但本行没有留下理由记录。' : 'Refused, but no rationale was recorded.')\n : (zh\n ? '已放行;本行没有留下理由记录(该请求未走到复核模型,或核可理由未落盘)。'\n : 'Allowed, but no rationale was recorded (the request never reached the reviewer, or its allow rationale was not persisted).')\n}\n\n\n/** Short wall-clock stamp. */\nfunction stamp(epochMs: number): string {\n const d = new Date(epochMs)\n const p = (n: number): string => String(n).padStart(2, '0')\n return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n}\n\n/** A labelled field row. */\nfunction Field({ label, children, mono }: {\n label: string\n children: React.ReactNode\n mono?: boolean\n}): React.JSX.Element {\n return (\n <div style={{ display: 'flex', gap: 10, alignItems: 'baseline' }}>\n <span style={{ color: MUTED, flex: '0 0 auto', width: 76, fontSize: 11 }}>{label}</span>\n <span style={{\n color: TEXT,\n fontSize: mono === true ? 11 : 12,\n fontFamily: mono === true ? CODE : undefined,\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n flex: '1 1 auto',\n }}>{children}</span>\n </div>\n )\n}\n\n/** Merge the base list with the loaded catalog, keeping the base order first. */\nfunction reviewerRoutesMerge(base: readonly string[], loaded: readonly string[]): readonly string[] {\n const out = [...base]\n for (const route of loaded) if (!out.includes(route)) out.push(route)\n return out\n}\n\n/** One ledger entry, expanded. */\nfunction Entry({ record, zh, onApprove, deniedIndex }: {\n record: ClientAuditRecord\n zh: boolean\n onApprove?: (record: ClientAuditRecord, denialIndex: number) => void\n deniedIndex: number\n}): React.JSX.Element {\n const [showArgs, setShowArgs] = useState(false)\n const pending = record.outcome === undefined\n const verdict = pending\n ? (zh ? '进行中' : 'pending')\n : record.refused\n ? (zh ? '否决' : 'refused')\n : record.outcome === 'allowed-once' ? (zh ? '放行' : 'allowed') : (zh ? '转人工' : 'delegated')\n const tone = pending ? MUTED : record.refused ? REFUSED : ALLOWED\n\n return (\n <div style={{\n border: `1px solid ${BORDER}`,\n borderLeft: `3px solid ${tone}`,\n borderRadius: 8,\n background: ROW,\n padding: '12px 14px',\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n }}>\n <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>\n <span style={{ fontFamily: CODE, fontSize: 13, fontWeight: 600, color: TEXT }}>{record.toolName}</span>\n <span style={{ fontSize: 11, color: tone, border: `1px solid ${tone}`, borderRadius: 999, padding: '1px 7px' }}>{verdict}</span>\n {routingTag(record, zh) === undefined ? null : (\n <span style={{ fontSize: 11, color: MUTED, border: `1px solid ${BORDER}`, borderRadius: 999, padding: '1px 7px' }}>\n {routingTag(record, zh)}\n </span>\n )}\n {record.risk === undefined ? null : (\n <span style={{ fontSize: 11, color: riskTone(record.risk) }}>{zh ? '风险' : 'risk'} {record.risk}</span>\n )}\n {record.overridden ? <span style={{ fontSize: 11, color: WARN }}>{zh ? '含人工一次性授权' : 'human override'}</span> : null}\n <span style={{ marginLeft: 'auto', fontSize: 11, color: MUTED }}>\n {stamp(record.startedAt)} · T{record.turn}/S{record.step}\n {record.durationMs === undefined ? '' : ` · ${record.durationMs} ms`}\n </span>\n </div>\n\n <Field label={zh ? '裁决理由' : 'rationale'}>\n <span style={{ color: record.reason === undefined ? MUTED : TEXT }}>{rationaleText(record, zh)}</span>\n </Field>\n\n {record.suggestion === undefined ? null : (\n <Field label={zh ? '更安全的做法' : 'safer path'}>{record.suggestion}</Field>\n )}\n <Field label={zh ? '路由策略' : 'routing'} mono>{record.policy} · {record.policySource}</Field>\n {record.askReason === undefined ? null : (\n <Field label={zh ? '申请理由' : 'asked why'}>{record.askReason}</Field>\n )}\n {record.reviewerRoute === undefined ? null : (\n <Field label={zh ? '复核模型' : 'reviewer'} mono>\n {record.reviewerRoute}{record.uncertain ? ` · ${zh ? '不确定' : 'uncertain'}` : ''}\n </Field>\n )}\n\n {record.argumentsPreview === undefined ? null : (\n <div>\n <button\n type=\"button\"\n onClick={() => setShowArgs(v => !v)}\n style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', color: 'var(--dsw-alias-link, #58a6ff)', fontSize: 11 }}\n >{showArgs ? (zh ? '收起参数' : 'hide arguments') : (zh ? '查看参数' : 'show arguments')}</button>\n {showArgs ? (\n <pre style={{\n margin: '6px 0 0', padding: 10, borderRadius: 6, background: PANEL,\n fontFamily: CODE, fontSize: 11, color: TEXT, whiteSpace: 'pre-wrap',\n wordBreak: 'break-word', maxHeight: 320, overflow: 'auto',\n }}>{record.argumentsPreview}</pre>\n ) : null}\n </div>\n )}\n\n {record.refused && record.outcome === 'rejected' && onApprove !== undefined ? (\n <button\n type=\"button\"\n onClick={() => onApprove(record, deniedIndex)}\n style={{\n alignSelf: 'flex-start', cursor: 'pointer', fontSize: 11, padding: '3px 9px',\n borderRadius: 6, border: `1px solid ${BORDER}`, background: 'transparent', color: TEXT,\n }}\n >{zh ? `授权重试第 ${deniedIndex} 条否决` : `approve denial #${deniedIndex} for one retry`}</button>\n ) : null}\n </div>\n )\n}\n\n/** The full ledger tab. */\n/**\n * Start a freshly opened ledger at its top.\n *\n * The tab renders inside the conversation's resident scrollport, which the\n * transcript keeps pinned to its newest line, so a ledger mounted under it would\n * show its own BOTTOM — while the ledger lists the newest decision FIRST. The\n * walk that finds the box to reset lives in `./scroll.ts`.\n * @param root - the ledger's root element.\n */\nfunction useStartAtTop(root: React.RefObject<HTMLDivElement | null>): void {\n useEffect(() => {\n const element = root.current\n if (element === null) return\n // Our own container first: it is the scroller whenever the height chain\n // reaches it, and a fresh mount already starts at zero there.\n element.scrollTop = 0\n if (typeof window === 'undefined') return\n resetScrollableAncestorToTop(element, node => window.getComputedStyle(node).overflowY)\n }, [root])\n}\n\nexport function LedgerView({ view, zh, runCommand, modelChoices, loadModelRoutes }: LedgerViewProps): React.JSX.Element {\n const rootRef = useRef<HTMLDivElement>(null)\n useStartAtTop(rootRef)\n const [loadedChoices, setLoadedChoices] = useState<readonly string[] | undefined>(undefined)\n /** Route id → the catalog's display name, when it differs from the id. */\n const [modelLabels, setModelLabels] = useState<Readonly<Record<string, string>>>({})\n // A refused command used to look identical to a click that did nothing.\n const [commandError, setCommandError] = useState<string | null>(null)\n const run = (line: string): void => {\n if (runCommand === undefined) return\n setCommandError(null)\n void Promise.resolve(runCommand(line))\n .then((failure) => { if (typeof failure === 'string') setCommandError(failure) })\n .catch((error: unknown) => { setCommandError(String(error)) })\n }\n // The base list (override in force + session model) paints immediately; the\n // catalog replaces it once loaded, so the picker is never empty in between.\n const choices = loadedChoices ?? modelChoices ?? []\n const records = view?.records ?? []\n const denials = useMemo(() => records.filter(r => r.refused), [records])\n const reviewedCount = useMemo(() => records.filter(r => r.policy === 'ai').length, [records])\n const deniedIndexOf = (record: ClientAuditRecord): number =>\n denials.findIndex(d => d.reviewId === record.reviewId) + 1\n\n const headerStyle: CSSProperties = {\n display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',\n padding: '0 2px 10px', borderBottom: `1px solid ${BORDER}`, marginBottom: 12,\n }\n\n return (\n <div ref={rootRef} style={{ padding: '14px 16px', overflow: 'auto', height: '100%', fontFamily: 'inherit' }}>\n <div style={headerStyle}>\n <strong style={{ fontSize: 14, color: TEXT }}>{zh ? '审批审计' : 'Approval audit'}</strong>\n <span style={{ fontSize: 12, color: view?.enabled === false ? MUTED : ALLOWED }}>\n {view === undefined ? (zh ? '尚无数据' : 'no data') : view.enabled ? (zh ? '自动审批已开启' : 'auto-approval on') : (zh ? '自动审批已关闭' : 'auto-approval off')}\n </span>\n {runCommand === undefined ? null : (\n <>\n <button type=\"button\" disabled={view?.enabled === true} onClick={() => run('/approval-review on')}\n style={{ fontSize: 11, padding: '3px 9px', borderRadius: 6, cursor: view?.enabled === true ? 'default' : 'pointer', border: `1px solid ${BORDER}`, color: view?.enabled === true ? MUTED : TEXT, background: 'transparent' }}>\n {zh ? '开启' : 'on'}\n </button>\n <button type=\"button\" disabled={view?.enabled === false} onClick={() => run('/approval-review off')}\n style={{ fontSize: 11, padding: '3px 9px', borderRadius: 6, cursor: view?.enabled === false ? 'default' : 'pointer', border: `1px solid ${BORDER}`, color: view?.enabled === false ? MUTED : TEXT, background: 'transparent' }}>\n {zh ? '关闭' : 'off'}\n </button>\n </>\n )}\n {view === undefined ? null : (\n <span style={{ fontSize: 11, color: MUTED, fontFamily: CODE }}>\n {zh ? '复核模型 ' : 'reviewer '}\n {view.reviewerModel.length > 0\n ? `${view.reviewerProvider.length > 0 ? `${view.reviewerProvider}/` : ''}${view.reviewerModel}`\n : (zh ? '继承会话' : 'inherit session')}\n </span>\n )}\n {runCommand === undefined ? null : (\n <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>\n <ModelPicker\n choices={choices}\n labels={modelLabels}\n runCommand={run}\n loadModelRoutes={loadModelRoutes}\n zh={zh}\n onRoutesLoaded={({ routes, labels }) => {\n setModelLabels(labels)\n setLoadedChoices(routes.length === 0 ? (modelChoices ?? []) : reviewerRoutesMerge(modelChoices ?? [], routes))\n }}\n />\n <button\n type=\"button\"\n onClick={() => run('/approval-review model default')}\n style={{ fontSize: 11, padding: '3px 9px', borderRadius: 6, cursor: 'pointer', border: `1px solid ${BORDER}`, color: TEXT, background: 'transparent' }}\n >{zh ? '继承' : 'inherit'}</button>\n </span>\n )}\n {view === undefined || view.total === 0 ? null : (\n <span style={{ fontSize: 11, color: MUTED }}>\n {zh\n ? `共 ${view.total} 次 · 本插件裁决 ${reviewedCount} · 已否决 ${view.refused} · 本回合复审 ${view.reviewsThisTurn}/${view.maxReviewsPerTurn} · 连续否决 ${view.consecutiveDenials}`\n : `${view.total} total · ${reviewedCount} routed to the reviewer · ${view.refused} refused · this turn ${view.reviewsThisTurn}/${view.maxReviewsPerTurn} · streak ${view.consecutiveDenials}`}\n </span>\n )}\n </div>\n\n {commandError === null ? null : (\n <div style={{ fontSize: 11, color: REFUSED, marginBottom: 8 }}>\n {zh ? '命令被拒:' : 'command refused: '}{commandError}\n </div>\n )}\n\n {view?.circuitOpen === true ? (\n <div style={{ fontSize: 12, color: REFUSED, marginBottom: 10 }}>\n {zh ? '否决熔断已触发:本回合后续请求转人工审批。' : 'Rejection breaker is open: later requests in this turn go to the human chain.'}\n </div>\n ) : null}\n\n {records.length === 0 ? (\n <div style={{ fontSize: 13, color: MUTED, padding: '24px 4px', lineHeight: 1.7 }}>\n {zh\n ? '本会话还没有审批记录。当某个动作需要越过沙箱边界时,这里会留下完整的裁决理由、风险等级与更安全的替代做法。'\n : 'No approvals recorded in this session yet. When an action needs to cross the sandbox boundary, its full rationale, risk grade, and safer alternative land here.'}\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>\n {records.map(record => (\n <Entry\n key={record.reviewId}\n record={record}\n zh={zh}\n deniedIndex={deniedIndexOf(record)}\n onApprove={runCommand === undefined\n ? undefined\n : (_r, index) => run(`/approval-review approve ${index}`)}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n","/**\n * The \"approve for me\" access-mode glyph, installed from this plugin.\n *\n * Background: the composer's access-mode menu draws a shield glyph for each\n * permission preset, and that glyph table is a CLOSED design set inside\n * `@deepseek-ai/dsh-client-ui-conversation` — a preset key outside it renders\n * with no icon at all, and the host cannot be asked for one (the `permissions`\n * projection carries value/name/description only). So a plugin that adds a\n * fourth preset gets a fourth menu row with no picture next to it.\n *\n * Two ways out exist. Editing the harness package is the tidy one, but it only\n * takes effect after that package is rebuilt, and it couples the harness to a\n * plugin-specific key. This module is the other one: the plugin decorates the\n * two buttons the menu renders, from the outside, with the same shield+eye mark.\n *\n * Why it decorates by ATTRIBUTE and not by inserting nodes: these buttons belong\n * to React. Inserting a child would put an unknown node where React expects its\n * own child list and would make a later re-render reconcile against DOM it never\n * produced. Setting a `data-*` attribute and drawing the glyph from a\n * plugin-owned stylesheet via `::before` leaves React's tree untouched — React\n * does not remove attributes it never set, and a remount simply loses the mark\n * until the next pass, which the observer re-applies.\n *\n * The shim YIELDS to the built-in glyph: when the harness glyph table already\n * covers this key (i.e. the package was rebuilt with it), the mark is removed\n * and the stylesheet draws nothing, so a rebuild never produces a double icon.\n *\n * Naming coupling: the preset key comes from `Config.reviewerPreset` and the\n * bundle patch's `permission.presets` entry, and the display name from that same\n * entry's `name`. The DOM exposes the NAME (the trigger's `aria-label`, a menu\n * row's text), never the key, so the label list below is what this shim matches.\n * Rename the preset and the glyph simply does not appear; the menu keeps\n * working, which is why this is a progressive enhancement and not a dependency.\n * @module dsh-approval-review/client/access-mode-glyph\n */\n\n/**\n * Display names the preset may carry, matched against the access-mode trigger's\n * `aria-label` (\"访问模式,当前:替我审批\" / \"Access mode, current: Approve for\n * me\") and against a menu row's own text. The bundle patch ships the Chinese\n * name; the English form is accepted so an English deployment still gets its\n * glyph.\n */\nconst PRESET_LABELS: readonly string[] = ['替我审批', 'Approve for me']\n\n/** Marks a decorated button; also the selector the stylesheet hangs off. */\nconst MARK_ATTRIBUTE = 'data-dsh-approval-review-glyph'\n\n/** The style element's identity, so a re-install replaces its own node. */\nconst STYLE_ATTRIBUTE = 'data-dsh-approval-review-glyph-style'\n\n/** The shield outline shared by every built-in access-mode glyph. */\nconst SHIELD_OUTLINE =\n 'M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z'\n\n/**\n * The glyph itself: the same shield as the built-in modes — the boundary is\n * unchanged — carrying an eye, because the reviewer looks at the action before\n * it crosses. Rendered as a MASK, so the mark takes `currentColor` from the\n * button exactly like the built-in `currentColor` SVGs do.\n */\nconst GLYPH_SVG = '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"none\">'\n + `<path d=\"${SHIELD_OUTLINE}\" stroke=\"#000\" stroke-width=\"1.31831\" stroke-linejoin=\"round\"/>`\n + '<path d=\"M5.348 6.58C6.224 7.773 7.029 8.375 8.2 8.375C9.371 8.375 10.176 7.773 11.052 6.58\" stroke=\"#000\" stroke-width=\"1.31831\" stroke-linecap=\"round\"/>'\n + '<path d=\"M5.348 6.58C6.224 5.387 7.029 4.785 8.2 4.785C9.371 4.785 10.176 5.387 11.052 6.58\" stroke=\"#000\" stroke-width=\"1.31831\" stroke-linecap=\"round\"/>'\n + '<circle cx=\"8.2\" cy=\"6.58\" r=\"0.95\" fill=\"#000\"/>'\n + '</svg>'\n\n/**\n * The one selector every pass and every mutation check uses.\n *\n * It is deliberately narrow: the trigger is addressed by SUBSTRING on the\n * `aria-label` (a native attribute test, not a JavaScript scan of every button\n * in the document), menu rows by role, and already-marked buttons — whichever\n * they are — by the mark itself, so a mode switch clears the old one.\n */\nconst TARGET_SELECTOR = [\n ...PRESET_LABELS.map(label => `button[aria-label*=\"${label}\"]`),\n 'button[role=\"menuitem\"]',\n `[${MARK_ATTRIBUTE}]`,\n].join(', ')\n\n/**\n * Build the stylesheet that draws the mark on a decorated button.\n *\n * The colour is not cosmetic guesswork: the built-in glyphs sit inside the\n * menu's `.itemIcon` span, which the design system tints\n * `--dsw-alias-label-tertiary`, while the button itself is\n * `--dsw-alias-label-primary` (brighter, and brighter still on the selected\n * row). A `::before` on the button therefore inherits the WRONG one, which is\n * why the mark looked white next to three grey shields. The trigger has no such\n * span — its built-in icon inherits the button — so it keeps `currentColor`.\n */\nfunction stylesheet(): string {\n const mask = `url(\"data:image/svg+xml,${encodeURIComponent(GLYPH_SVG)}\")`\n return `\n[${MARK_ATTRIBUTE}]::before{\n content:\"\";\n display:inline-block;\n flex:none;\n width:16px;\n height:16px;\n background-color:currentColor;\n -webkit-mask-image:${mask};\n mask-image:${mask};\n -webkit-mask-repeat:no-repeat;\n mask-repeat:no-repeat;\n -webkit-mask-position:center;\n mask-position:center;\n -webkit-mask-size:contain;\n mask-size:contain;\n}\n/* A menu row's icon slot is tinted tertiary; match it instead of the button. */\nbutton[role=\"menuitem\"][${MARK_ATTRIBUTE}]::before{\n background-color:var(--dsw-alias-label-tertiary, currentColor);\n}\n/* The composer trigger sizes its icons at 14px and lets them inherit. */\nbutton[aria-label][${MARK_ATTRIBUTE}]::before{\n width:14px;\n height:14px;\n}\n`\n}\n\n/** Exported for tests: the exact stylesheet the shim installs. */\nexport function __stylesheetForTest(): string {\n return stylesheet()\n}\n\n/**\n * Whether an element already carries the harness's own glyph.\n *\n * Both surfaces render the built-in icon as a leading `<span>` holding an\n * `<svg>`; the trailing chevron is a later sibling, so only the first child\n * counts. When this is true the shim steps aside.\n */\nfunction hasBuiltInGlyph(button: HTMLElement): boolean {\n const first = button.firstElementChild\n return first instanceof HTMLElement\n && first.tagName === 'SPAN'\n && first.querySelector('svg') !== null\n}\n\n/**\n * The pure matching rule behind the shim.\n *\n * Extracted from the DOM pass so the rule that can actually go wrong — which\n * button is this plugin's preset, and does it already have a built-in glyph —\n * is testable without a browser. The DOM *plumbing* around it is decoration and\n * degrades to \"no glyph\", never to a broken menu.\n * @param facts - the element facts the shim matches on.\n * @returns `'mark'` when this plugin should draw the glyph, `'skip'` otherwise.\n */\nexport function accessModeGlyphDecision(facts: {\n readonly ariaLabel?: string | null\n readonly role?: string | null\n readonly text?: string\n readonly hasBuiltInGlyph?: boolean\n}): 'mark' | 'skip' {\n const label = facts.ariaLabel ?? ''\n const isTarget = PRESET_LABELS.some(candidate => label.includes(candidate))\n || (facts.role === 'menuitem' && PRESET_LABELS.includes((facts.text ?? '').trim()))\n if (!isTarget) return 'skip'\n return facts.hasBuiltInGlyph === true ? 'skip' : 'mark'\n}\n\n/**\n * Install the access-mode glyph decoration.\n *\n * The observer is deliberately cheap: it inspects only MUTATED subtrees for the\n * target selector and coalesces every hit into one animation frame, so a\n * streaming conversation (which appends text nodes constantly) never becomes a\n * per-token query over the document.\n * @param root - document to decorate; injectable for tests.\n * @returns the disposer that removes the stylesheet, the marks, and the observer.\n */\nexport function installAccessModeGlyph(root: Document = document): () => void {\n if (root.querySelector(`style[${STYLE_ATTRIBUTE}]`) === null) {\n const style = root.createElement('style')\n style.setAttribute(STYLE_ATTRIBUTE, '1')\n style.textContent = stylesheet()\n root.head.appendChild(style)\n }\n\n const decorate = (): void => {\n for (const button of root.querySelectorAll<HTMLElement>(TARGET_SELECTOR)) {\n const decision = accessModeGlyphDecision({\n ariaLabel: button.getAttribute('aria-label'),\n role: button.getAttribute('role'),\n text: button.textContent ?? '',\n hasBuiltInGlyph: hasBuiltInGlyph(button),\n })\n if (decision === 'mark') {\n if (!button.hasAttribute(MARK_ATTRIBUTE)) button.setAttribute(MARK_ATTRIBUTE, '1')\n } else if (button.hasAttribute(MARK_ATTRIBUTE)) {\n // Either the harness grew its own glyph, or the access mode moved on.\n // Both mean this plugin's mark no longer belongs on this button.\n button.removeAttribute(MARK_ATTRIBUTE)\n }\n }\n }\n\n /** True when a mutation could have produced one of the decorated buttons. */\n const mightMatter = (node: Node): boolean => {\n if (!(node instanceof Element)) return false\n if (node.matches(TARGET_SELECTOR)) return true\n return node.querySelector(TARGET_SELECTOR) !== null\n }\n\n let frame: number | undefined\n const schedule = (): void => {\n if (frame !== undefined) return\n frame = root.defaultView?.requestAnimationFrame(() => {\n frame = undefined\n decorate()\n })\n }\n\n const observer = new MutationObserver(records => {\n for (const record of records) {\n // A change INSIDE a target button matters too: React growing the built-in\n // icon inserts a span, not a button, and this shim has to notice that and\n // step aside — otherwise a harness rebuild paints two glyphs.\n const target = record.target\n if (target instanceof Element && target.closest(TARGET_SELECTOR) !== null) {\n schedule()\n return\n }\n for (const node of record.addedNodes) {\n if (mightMatter(node)) {\n schedule()\n return\n }\n }\n }\n })\n // A client bundle can execute before `body` exists; the initial pass still\n // runs, and the observer simply arrives with the next install.\n if (root.body !== null) observer.observe(root.body, { childList: true, subtree: true })\n\n // The initial pass covers a composer that rendered before this plugin mounted.\n decorate()\n\n return () => {\n observer.disconnect()\n if (frame !== undefined) root.defaultView?.cancelAnimationFrame(frame)\n for (const button of root.querySelectorAll<HTMLElement>(`[${MARK_ATTRIBUTE}]`)) {\n button.removeAttribute(MARK_ATTRIBUTE)\n }\n root.querySelector(`style[${STYLE_ATTRIBUTE}]`)?.remove()\n }\n}\n","/**\n * One slash-command line sent from this plugin's browser half to the host.\n *\n * The remote facade's arity is the whole point of this module. The host\n * declares\n *\n * ```ts\n * // packages/interaction/commands/src/index.ts\n * @Remote\n * async execute(agent, line, submittedAttachments, signal)\n * ```\n *\n * — three business arguments, because a command may carry submitted\n * attachments. Calling it with two does not fail at compile time against a\n * HAND-WRITTEN structural type (this plugin declares its own), it fails at\n * runtime with `commands/execute expected 3 business argument(s) … got 2`, and\n * every button in the tab becomes a silent no-op. That is exactly what happened,\n * so the call and its error mapping live here with tests.\n * @module dsh-approval-review/client/run-command\n */\n\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\n\n/** One command outcome as the remote facade reports it. */\nexport interface CommandRemoteResult {\n /** The handler's settled result, or undefined when the line did not resolve. */\n readonly value?: {\n readonly result: { readonly kind?: string; readonly text?: string }\n } | undefined\n}\n\n/** The command remote face this plugin drives. */\nexport interface CommandRemoteFace {\n readonly commands: {\n /**\n * Execute one command line in a session.\n * @param sessionId - the session the command runs in.\n * @param line - the full line, leading slash included.\n * @param attachments - submitted attachments; always `[]` for this plugin.\n * @returns the remote wrapper: `ok` discriminates refusal, `value` absence\n * means the host never resolved the line.\n */\n execute(\n sessionId: SessionId,\n line: string,\n attachments: readonly unknown[],\n ): Promise<\n | { readonly ok: true; readonly value: CommandRemoteResult['value'] }\n | { readonly ok: false; readonly error: { readonly code: string; readonly message: string } }\n >\n }\n}\n\n/**\n * Send one command line and reduce the outcome to \"failure text, or null\".\n *\n * A handler that answered `kind: 'error'` is a REFUSAL the operator must see\n * (`/approval-review nonsense` says so), while an unresolved line means the host\n * does not know the command at all. Both are returned as text, never thrown, so\n * the tab can show them next to the control that failed.\n * @param remote - the command remote, when the client has mounted it.\n * @param sessionId - the session the command runs in.\n * @param line - the full command line.\n * @returns null on success; a human-readable failure line otherwise.\n */\nexport async function runCommandLine(\n remote: CommandRemoteFace['commands'] | undefined,\n sessionId: SessionId,\n line: string,\n): Promise<string | null> {\n if (remote === undefined) return 'the command remote is not mounted in this client'\n try {\n const result = await remote.execute(sessionId, line, [])\n if (!result.ok) return `${result.error.message} (${result.error.code})`\n if (result.value === undefined) return `the host did not resolve \"${line}\"`\n const text = result.value.result.text\n return result.value.result.kind === 'error'\n ? (text !== undefined && text.length > 0 ? text : `\"${line}\" was refused`)\n : null\n } catch (error: unknown) {\n return `\"${line}\" failed: ${String(error)}`\n }\n}\n","/**\n * Browser half of the automatic-approval control surface.\n *\n * Contributes:\n * 1. a conversation tab — the approval ledger as a full page, beside 轨迹 /\n * 上下文 / 费用: every request, the reviewer's verdict and rationale, the\n * routing policy, risk, route, timing, and the live counters;\n * 2. the access-mode glyph for this plugin's `approve-for-me` preset.\n *\n * The session-header card this plugin used to contribute is gone: it duplicated\n * the tab's ledger in a popover that the tab already renders full-page, and the\n * header is the most contended strip of the session chrome.\n *\n * It also decorates the access-mode control with the glyph for this plugin's\n * `approve-for-me` preset, which the harness's closed glyph table cannot know\n * about (see `./access-mode-glyph.ts`).\n *\n * The host provides data through the `approvalReview` session projection, so\n * this half reads only whole projection values. Both surfaces drive the host\n * through the slash command — the same path a human typing it would take — which\n * keeps this half free of any assumption about the host's internal services.\n * @module dsh-approval-review/client\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\n// Type-only: the slot-scope augmentation that types `ctx.slots`, the session\n// standard props (`useProjection`), and the composer dock slot name.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { LedgerView } from './LedgerView.tsx'\nimport { installAccessModeGlyph } from './access-mode-glyph.ts'\nimport { directoryRoutes, reviewerRouteChoices } from './model-choices.ts'\nimport { runCommandLine, type CommandRemoteFace } from './run-command.ts'\nimport type { ClientAuditView } from './types.ts'\n\n/** Slot entry id; stable so a redeploy replaces its own row. */\nexport const VIEW_SLOT_ID = 'approval-review-ledger'\n\n/** The conversation tab strip, beside 轨迹 / 上下文 / 费用. */\nconst VIEW_SLOT = 'conversation.view'\n\n/**\n * Required client services.\n *\n * `remote.commands` is a named remote SERVICE, not a plain property: the client\n * remote facade throws `cannot get property \"remote.commands\" without inject`\n * unless the key is declared here. Declaring `remote` alone is not enough, which\n * is exactly the bug this cost once.\n */\nexport const inject = ['slots', 'remote', 'remote.commands']\n\n/**\n * The client model directory this harness publishes for model picking.\n *\n * Read structurally and OPTIONALLY: it is what the composer's model seat and the\n * `/model` picker use, so it is the deployment's own answer to \"which models are\n * configured here\". `subagentModelSelectionPolicy` would be the tighter list,\n * but it is host-only (no `wire`), so it never reaches the browser.\n */\ninterface ModelDirectoriesFace {\n /**\n * @param sessionId - the session whose catalog to load.\n * @returns the loaded directory snapshot.\n */\n directoryFor(sessionId: SessionId): {\n load(): Promise<{ readonly groups?: unknown }>\n }\n}\n\n/** Props the framework supplies to a session-scoped slot entry. */\ninterface SessionActionProps {\n /** Host-computed projection values addressed by key. */\n readonly useProjection: (key: string) => unknown\n /** Current session identity, absent while no session is selected. */\n readonly sessionId?: SessionId\n}\n\n/** Whether copy should be Chinese, from the browser language. */\nfunction preferZh(): boolean {\n return typeof navigator !== 'undefined' && navigator.language.toLowerCase().startsWith('zh')\n}\n\n/**\n * The business face the slot framework injects per session. Carrying the session\n * id here — rather than in module state — is what makes the control correct for\n * whichever session it is rendered in.\n */\nexport interface ApprovalReviewInjected {\n /**\n * Load the locally configured reviewer routes with their display names.\n * @returns route ids plus a route→display-name map.\n */\n loadModelRoutes: () => Promise<{ readonly routes: readonly string[]; readonly labels: Readonly<Record<string, string>> }>\n /**\n * Execute one approval-review command line in this session.\n * @param line - the full command line, leading slash included.\n * @returns null when the host admitted it; a failure line otherwise.\n */\n runCommand: (line: string) => Promise<string | null>\n}\n\n/** Reviewer routes this deployment offers, read from its own projections. */\nfunction reviewerChoices(props: SessionActionProps, current: string | undefined): readonly string[] {\n return reviewerRouteChoices({\n current,\n sessionDefault: props.useProjection('modelSelection'),\n allowed: props.useProjection('subagentModelSelectionPolicy'),\n })\n}\n\n/** Render the full ledger tab. */\nfunction ApprovalReviewLedger(props: SessionActionProps & ApprovalReviewInjected): React.JSX.Element {\n const view = props.useProjection('approvalReview') as ClientAuditView | undefined\n const current = view === undefined || view.reviewerModel.length === 0\n ? undefined\n : `${view.reviewerProvider.length > 0 ? `${view.reviewerProvider}/` : ''}${view.reviewerModel}`\n return LedgerView({\n view,\n zh: preferZh(),\n runCommand: props.runCommand,\n modelChoices: reviewerChoices(props, current),\n loadModelRoutes: props.loadModelRoutes,\n })\n}\n\n/**\n * Register the ledger tab and the access-mode glyph.\n * @param ctx - client Cordis context.\n */\nexport function apply(ctx: ClientContext): void {\n // The glyph is decoration over a control this plugin's preset is part of; it\n // owns a style element, a few attributes, and one observer, all released by\n // the effect when the plugin unmounts or reloads.\n ctx.effect(() => installAccessModeGlyph(), 'approval-review: access-mode glyph')\n /**\n * The remote is resolved LAZILY, per call. Capturing `ctx.remote` in the\n * `apply` closure is wrong: `apply` can run before the remote facade finishes\n * mounting, and a captured `undefined` turns every click into a silent no-op\n * on a control that still LOOKS enabled. That was a real bug here.\n */\n const remoteOf = (): CommandRemoteFace['commands'] | undefined =>\n (ctx as unknown as { remote?: CommandRemoteFace }).remote?.commands\n\n /**\n * The model directory, resolved LAZILY for the same reason the command remote\n * is: this client half mounts before every service it may use is up, and a\n * captured `undefined` would permanently disable the picker.\n */\n const directoriesOf = (): ModelDirectoriesFace | undefined =>\n (ctx as unknown as { get?: (name: string) => unknown }).get?.('modelDirectories') as ModelDirectoriesFace | undefined\n\n /** The per-session business face the tab's seat uses. */\n // The seat hands the session id as a plain string; the command remote takes\n // the branded id, so the brand is reasserted at this one boundary.\n const inject = (rawSessionId: string): ApprovalReviewInjected => ({\n loadModelRoutes: async () => {\n const directories = directoriesOf()\n if (directories === undefined) return { routes: [], labels: {} }\n try {\n // The catalog load is shared and cached by the harness, so opening the\n // picker costs nothing after the composer's own model seat has loaded.\n return directoryRoutes(await directories.directoryFor(rawSessionId as SessionId).load())\n } catch {\n return { routes: [], labels: {} }\n }\n },\n runCommand: async (line: string) => {\n // The remote is resolved per call (see `remoteOf`), and the line goes out\n // through `runCommandLine`, which owns the 3-business-argument arity and\n // the refusal mapping.\n return await runCommandLine(remoteOf(), rawSessionId as SessionId, line)\n },\n })\n\n ctx.slots.inject(VIEW_SLOT, () => ctx.slots.register(\n {\n name: VIEW_SLOT,\n id: VIEW_SLOT_ID,\n // After the built-in 轨迹 tab so the strip keeps its familiar order.\n order: 40,\n label: () => (preferZh() ? '审批' : 'Approvals'),\n inject,\n },\n ApprovalReviewLedger,\n ))\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -80,7 +80,7 @@ const Config = Schema.object({
80
80
  "rejected",
81
81
  "delegate",
82
82
  "allow-once"
83
- ]).default("rejected").description("Reaction when the reviewer crashes, times out, or answers off-schema."),
83
+ ]).default("delegate").description("Reaction when the reviewer crashes, times out, or answers off-schema. `delegate` (default) hands the request to the human chain: a reviewer that could not run is an infrastructure problem, not a judgement. `rejected` refuses instead."),
84
84
  budget: Schema.object({
85
85
  maxReviewsPerTurn: Schema.number().step(1).min(1).default(20).description("Maximum reviewer calls per open turn."),
86
86
  onExhausted: Schema.union(["delegate", "deny"]).default("delegate").description("Reaction once the per-turn review budget is spent.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-approval-review",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Codex-style agent auto-approval for DeepSeek Harness: an independent reviewer model decides allow/deny on the approval answerer chain, fail-closed, with a per-decision rationale — refusals and allows alike — in a dedicated Approvals tab. · DSH 插件:Codex 风格的 Agent 自动审批,独立 reviewer 模型裁决、fail-closed、每次审批(放行与否决)都留下详细理由,并在独立的「审批」页签中展示。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",