dsh-approval-review 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -576,6 +576,16 @@ window.__ModuleLoader__.load({
576
576
  const rootRef = (0, react.useRef)(null);
577
577
  useStartAtTop(rootRef);
578
578
  const [loadedChoices, setLoadedChoices] = (0, react.useState)(void 0);
579
+ const [commandError, setCommandError] = (0, react.useState)(null);
580
+ const run = (line) => {
581
+ if (runCommand === void 0) return;
582
+ setCommandError(null);
583
+ Promise.resolve(runCommand(line)).then((failure) => {
584
+ if (typeof failure === "string") setCommandError(failure);
585
+ }).catch((error) => {
586
+ setCommandError(String(error));
587
+ });
588
+ };
579
589
  const choices = loadedChoices ?? modelChoices ?? [];
580
590
  const records = view?.records ?? [];
581
591
  const denials = (0, react.useMemo)(() => records.filter((r) => r.refused), [records]);
@@ -618,7 +628,7 @@ window.__ModuleLoader__.load({
618
628
  runCommand === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
619
629
  type: "button",
620
630
  disabled: view?.enabled === true,
621
- onClick: () => runCommand("/approval-review on"),
631
+ onClick: () => run("/approval-review on"),
622
632
  style: {
623
633
  fontSize: 11,
624
634
  padding: "3px 9px",
@@ -632,7 +642,7 @@ window.__ModuleLoader__.load({
632
642
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
633
643
  type: "button",
634
644
  disabled: view?.enabled === false,
635
- onClick: () => runCommand("/approval-review off"),
645
+ onClick: () => run("/approval-review off"),
636
646
  style: {
637
647
  fontSize: 11,
638
648
  padding: "3px 9px",
@@ -660,7 +670,7 @@ window.__ModuleLoader__.load({
660
670
  },
661
671
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
662
672
  choices,
663
- runCommand,
673
+ runCommand: run,
664
674
  loadModels,
665
675
  zh,
666
676
  onChoicesLoaded: (routes) => {
@@ -668,7 +678,7 @@ window.__ModuleLoader__.load({
668
678
  }
669
679
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
670
680
  type: "button",
671
- onClick: () => runCommand("/approval-review model default"),
681
+ onClick: () => run("/approval-review model default"),
672
682
  style: {
673
683
  fontSize: 11,
674
684
  padding: "3px 9px",
@@ -690,6 +700,14 @@ window.__ModuleLoader__.load({
690
700
  })
691
701
  ]
692
702
  }),
703
+ commandError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
704
+ style: {
705
+ fontSize: 11,
706
+ color: REFUSED,
707
+ marginBottom: 8
708
+ },
709
+ children: [zh ? "命令被拒:" : "command refused: ", commandError]
710
+ }),
693
711
  view?.circuitOpen === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
694
712
  style: {
695
713
  fontSize: 12,
@@ -716,7 +734,7 @@ window.__ModuleLoader__.load({
716
734
  record,
717
735
  zh,
718
736
  deniedIndex: deniedIndexOf(record),
719
- onApprove: runCommand === void 0 ? void 0 : (_r, index) => runCommand(`/approval-review approve ${index}`)
737
+ onApprove: runCommand === void 0 ? void 0 : (_r, index) => run(`/approval-review approve ${index}`)
720
738
  }, record.reviewId))
721
739
  })
722
740
  ]
@@ -911,6 +929,32 @@ button[aria-label][${MARK_ATTRIBUTE}]::before{
911
929
  };
912
930
  }
913
931
  //#endregion
932
+ //#region src/client/run-command.ts
933
+ /**
934
+ * Send one command line and reduce the outcome to "failure text, or null".
935
+ *
936
+ * A handler that answered `kind: 'error'` is a REFUSAL the operator must see
937
+ * (`/approval-review nonsense` says so), while an unresolved line means the host
938
+ * does not know the command at all. Both are returned as text, never thrown, so
939
+ * the tab can show them next to the control that failed.
940
+ * @param remote - the command remote, when the client has mounted it.
941
+ * @param sessionId - the session the command runs in.
942
+ * @param line - the full command line.
943
+ * @returns null on success; a human-readable failure line otherwise.
944
+ */
945
+ async function runCommandLine(remote, sessionId, line) {
946
+ if (remote === void 0) return "the command remote is not mounted in this client";
947
+ try {
948
+ const result = await remote.execute(sessionId, line, []);
949
+ if (!result.ok) return `${result.error.message} (${result.error.code})`;
950
+ if (result.value === void 0) return `the host did not resolve "${line}"`;
951
+ const text = result.value.result.text;
952
+ return result.value.result.kind === "error" ? text !== void 0 && text.length > 0 ? text : `"${line}" was refused` : null;
953
+ } catch (error) {
954
+ return `"${line}" failed: ${String(error)}`;
955
+ }
956
+ }
957
+ //#endregion
914
958
  //#region src/client/index.tsx
915
959
  /** Slot entry id; stable so a redeploy replaces its own row. */
916
960
  const VIEW_SLOT_ID = "approval-review-ledger";
@@ -984,15 +1028,7 @@ button[aria-label][${MARK_ATTRIBUTE}]::before{
984
1028
  }
985
1029
  },
986
1030
  runCommand: async (line) => {
987
- const commands = remoteOf();
988
- if (commands === void 0) return "the command remote is not mounted in this client";
989
- const sessionId = rawSessionId;
990
- try {
991
- if (!(await commands.execute(sessionId, line)).ok) return `the host refused "${line}"`;
992
- return null;
993
- } catch (error) {
994
- return `"${line}" failed: ${String(error)}`;
995
- }
1031
+ return await runCommandLine(remoteOf(), rawSessionId, line);
996
1032
  }
997
1033
  });
998
1034
  ctx.slots.inject(VIEW_SLOT, () => ctx.slots.register({
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/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 * 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 if (typeof value !== 'object' || value === null) return []\n const groups = (value as { readonly groups?: unknown }).groups\n if (!Array.isArray(groups)) return []\n const out: string[] = []\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 (!out.includes(route)) out.push(route)\n }\n }\n return out\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 /** 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 loadModels?: (() => Promise<readonly string[]>) | 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 onChoicesLoaded: (routes: readonly string[]) => 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, runCommand, loadModels, zh, onChoicesLoaded }: 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 || loadModels === undefined) return\n requestedRef.current = true\n void loadModels()\n .then(routes => onChoicesLoaded(routes))\n .catch(() => onChoicesLoaded([]))\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 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 >{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 /** Runs one slash command line in this session. */\n readonly runCommand?: (line: string) => void\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. Called when the picker\n * opens, so the catalog is only fetched when someone actually picks a model.\n */\n readonly loadModels?: () => Promise<readonly string[]>\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, loadModels }: LedgerViewProps): React.JSX.Element {\n const rootRef = useRef<HTMLDivElement>(null)\n useStartAtTop(rootRef)\n const [loadedChoices, setLoadedChoices] = useState<readonly string[] | undefined>(undefined)\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={() => runCommand('/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={() => runCommand('/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 runCommand={runCommand}\n loadModels={loadModels}\n zh={zh}\n onChoicesLoaded={(routes) => {\n setLoadedChoices(routes.length === 0 ? (modelChoices ?? []) : reviewerRoutesMerge(modelChoices ?? [], routes))\n }}\n />\n <button\n type=\"button\"\n onClick={() => runCommand('/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 {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) => runCommand(`/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 * 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 { reviewerRouteChoices, routesFromDirectory } from './model-choices.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 command-remote subset this half drives. Spelled structurally so the\n * browser half keeps building against the harness packages it truly needs.\n */\ninterface CommandRemoteFace {\n readonly commands: {\n /**\n * Execute one slash-command line in a session.\n * @param sessionId - the session the command runs in.\n * @param line - the full command line, leading slash included.\n * @returns the remote outcome; `ok` is false when the host refused it.\n */\n execute(sessionId: SessionId, line: string): Promise<{ readonly ok: boolean }>\n }\n}\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 * 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 * Load the locally configured reviewer routes, as `provider/model`.\n * @returns the catalog's routes, or an empty list when unavailable.\n */\n loadModels: () => Promise<readonly string[]>\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 loadModels: props.loadModels,\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 loadModels: async () => {\n const directories = directoriesOf()\n if (directories === undefined) return []\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 routesFromDirectory(await directories.directoryFor(rawSessionId as SessionId).load())\n } catch {\n return []\n }\n },\n runCommand: async (line: string) => {\n const commands = remoteOf()\n if (commands === undefined) return 'the command remote is not mounted in this client'\n const sessionId = rawSessionId as SessionId\n try {\n const result = await commands.execute(sessionId, line)\n if (!result.ok) return `the host refused \"${line}\"`\n return null\n } catch (error: unknown) {\n return `\"${line}\" failed: ${String(error)}`\n }\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 * 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 if (typeof value !== 'object' || value === null) return []\n const groups = (value as { readonly groups?: unknown }).groups\n if (!Array.isArray(groups)) return []\n const out: string[] = []\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 (!out.includes(route)) out.push(route)\n }\n }\n return out\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 /** 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 loadModels?: (() => Promise<readonly string[]>) | 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 onChoicesLoaded: (routes: readonly string[]) => 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, runCommand, loadModels, zh, onChoicesLoaded }: 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 || loadModels === undefined) return\n requestedRef.current = true\n void loadModels()\n .then(routes => onChoicesLoaded(routes))\n .catch(() => onChoicesLoaded([]))\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 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 >{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. Called when the picker\n * opens, so the catalog is only fetched when someone actually picks a model.\n */\n readonly loadModels?: () => Promise<readonly string[]>\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, loadModels }: LedgerViewProps): React.JSX.Element {\n const rootRef = useRef<HTMLDivElement>(null)\n useStartAtTop(rootRef)\n const [loadedChoices, setLoadedChoices] = useState<readonly string[] | undefined>(undefined)\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 runCommand={run}\n loadModels={loadModels}\n zh={zh}\n onChoicesLoaded={(routes) => {\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 { reviewerRouteChoices, routesFromDirectory } 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 * 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 * Load the locally configured reviewer routes, as `provider/model`.\n * @returns the catalog's routes, or an empty list when unavailable.\n */\n loadModels: () => Promise<readonly string[]>\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 loadModels: props.loadModels,\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 loadModels: async () => {\n const directories = directoriesOf()\n if (directories === undefined) return []\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 routesFromDirectory(await directories.directoryFor(rawSessionId as SessionId).load())\n } catch {\n return []\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-approval-review",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",