dsh-approval-review 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README-zh.md +240 -0
- package/README.md +333 -0
- package/cordis.patch.yml +127 -0
- package/lib/client.js +1014 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +321 -0
- package/lib/index.js +2214 -0
- package/package.json +103 -0
|
@@ -0,0 +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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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
import "@deepseek-ai/dsh-session";
|
|
4
|
+
import "@deepseek-ai/dsh-session-projection";
|
|
5
|
+
//#region src/review-types.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Domain vocabulary for the approval-review capability: how an approval request
|
|
8
|
+
* is classified, what the reviewer answered, and the record that the audit card
|
|
9
|
+
* renders. Kept free of Cordis and host-service imports so the pure rules and
|
|
10
|
+
* the browser half can share it.
|
|
11
|
+
* @module dsh-approval-review/review-types
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Which answerer owns an approval request.
|
|
15
|
+
*
|
|
16
|
+
* - `ai` — this plugin's reviewer model decides (`allowed-once` / `rejected`).
|
|
17
|
+
* - `human` — delegate via `next()` to the rest of the answerer chain (a UI
|
|
18
|
+
* prompt, ACP, …); the plugin never short-circuits the chain.
|
|
19
|
+
* - `never` — deterministic `rejected` with an explanatory marker, no reviewer
|
|
20
|
+
* call and no human prompt. The "hard disable" stance for a tool family.
|
|
21
|
+
*/
|
|
22
|
+
type ToolPolicy = 'ai' | 'human' | 'never';
|
|
23
|
+
/** Risk grades a reviewer may report, ordered from least to most dangerous. */
|
|
24
|
+
type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/config.d.ts
|
|
27
|
+
/** How the plugin reacts to a reviewer verdict that exceeds the risk threshold. */
|
|
28
|
+
type RiskGateAction = 'allow' | 'delegate' | 'deny';
|
|
29
|
+
/** How the plugin reacts when the reviewer cannot decide. */
|
|
30
|
+
type UncertaintyAction = 'delegate' | 'allow' | 'deny';
|
|
31
|
+
/** What happens to a request the reviewer never answered (crash, timeout, bad schema). */
|
|
32
|
+
type FallbackAction = 'rejected' | 'delegate' | 'allow-once';
|
|
33
|
+
/** What happens once the per-turn review budget is spent. */
|
|
34
|
+
type BudgetAction = 'delegate' | 'deny';
|
|
35
|
+
/** What happens once the rejection circuit breaker trips. */
|
|
36
|
+
type CircuitAction = 'delegate' | 'deny';
|
|
37
|
+
/** One ordered regex rule that routes a request by matching its text. */
|
|
38
|
+
interface RiskRuleConfig {
|
|
39
|
+
/** Regular expression source, matched against {@link field}. */
|
|
40
|
+
readonly pattern: string;
|
|
41
|
+
/** Policy applied when the pattern matches. */
|
|
42
|
+
readonly policy: ToolPolicy;
|
|
43
|
+
/** Which text the pattern is matched against. Defaults to the ask reason. */
|
|
44
|
+
readonly field?: 'reason' | 'toolName' | 'arguments';
|
|
45
|
+
/** Optional human-readable note carried into the audit record. */
|
|
46
|
+
readonly note?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Reviewer model and prompt configuration. */
|
|
49
|
+
/** How the reviewer is run. */
|
|
50
|
+
type ReviewerMode = 'subagent' | 'direct';
|
|
51
|
+
interface ReviewerConfig {
|
|
52
|
+
/** `subagent` runs a read-only child; `direct` makes one plain model call. */
|
|
53
|
+
readonly mode: ReviewerMode;
|
|
54
|
+
/** Provider route for the reviewer; unset inherits the calling agent's provider. */
|
|
55
|
+
readonly provider?: string;
|
|
56
|
+
/** Model id for the reviewer; unset inherits the calling agent's model. */
|
|
57
|
+
readonly model?: string;
|
|
58
|
+
/** Subagent backend used by `mode: 'subagent'`. */
|
|
59
|
+
readonly subagentProvider: string;
|
|
60
|
+
/**
|
|
61
|
+
* The reviewer child's tool allow-list. Mutable to match Schemastery's
|
|
62
|
+
* inferred `string[]`; the plugin never writes it.
|
|
63
|
+
*/
|
|
64
|
+
readonly tools: string[];
|
|
65
|
+
/** Hard deadline for one reviewer call. */
|
|
66
|
+
readonly timeoutMs: number;
|
|
67
|
+
/** Output-token cap for one reviewer call. */
|
|
68
|
+
readonly maxTokens: number;
|
|
69
|
+
/** Sampling temperature; the reviewer should be near-deterministic. */
|
|
70
|
+
readonly temperature: number;
|
|
71
|
+
/** Ruling policy appended to the reviewer prompt (Codex-style policy text). */
|
|
72
|
+
readonly policyText?: string;
|
|
73
|
+
/** Extra deployment-specific guidance appended after {@link policyText}. */
|
|
74
|
+
readonly guidance?: string;
|
|
75
|
+
/** Max characters of one stringified argument value before it is truncated. */
|
|
76
|
+
readonly argumentMaxChars: number;
|
|
77
|
+
/** Cross-field total argument budget in characters; 0 disables the cap. */
|
|
78
|
+
readonly argumentsBudgetChars: number;
|
|
79
|
+
}
|
|
80
|
+
/** Compact-transcript budget handed to the reviewer as evidence. */
|
|
81
|
+
interface ContextConfig {
|
|
82
|
+
/** How many prior turns of history to include; 0 sends no transcript. */
|
|
83
|
+
readonly turns: number;
|
|
84
|
+
/** Character budget for the whole transcript section. */
|
|
85
|
+
readonly maxChars: number;
|
|
86
|
+
/** Include assistant messages in the transcript. */
|
|
87
|
+
readonly includeAssistant: boolean;
|
|
88
|
+
/** Include tool call/result pairs in the transcript. */
|
|
89
|
+
readonly includeToolActivity: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** Verdict cache settings. */
|
|
92
|
+
interface VerdictCacheConfig {
|
|
93
|
+
/** How long a cached verdict stays usable; 0 disables the cache. */
|
|
94
|
+
readonly ttlMs: number;
|
|
95
|
+
/** Maximum cached fingerprints before oldest-eviction. */
|
|
96
|
+
readonly maxEntries: number;
|
|
97
|
+
}
|
|
98
|
+
/** Per-turn reviewer budget. */
|
|
99
|
+
interface BudgetConfig {
|
|
100
|
+
/** Maximum reviewer calls per open turn. */
|
|
101
|
+
readonly maxReviewsPerTurn: number;
|
|
102
|
+
/** What happens once the budget is spent. */
|
|
103
|
+
readonly onExhausted: BudgetAction;
|
|
104
|
+
}
|
|
105
|
+
/** Rejection circuit breaker, mirroring Codex's per-turn denial breaker. */
|
|
106
|
+
interface CircuitBreakerConfig {
|
|
107
|
+
/** Consecutive denials that trip the breaker. */
|
|
108
|
+
readonly consecutiveDenials: number;
|
|
109
|
+
/** Denials within {@link windowSize} that trip the breaker; 0 disables. */
|
|
110
|
+
readonly windowDenials: number;
|
|
111
|
+
/** Rolling window size for {@link windowDenials}. */
|
|
112
|
+
readonly windowSize: number;
|
|
113
|
+
/** What happens once the breaker is open. */
|
|
114
|
+
readonly action: CircuitAction;
|
|
115
|
+
}
|
|
116
|
+
/** One-shot human override. */
|
|
117
|
+
interface OverrideConfig {
|
|
118
|
+
/** How long an `/approve` authorization stays usable. */
|
|
119
|
+
readonly ttlMs: number;
|
|
120
|
+
/** How many recent denials the override selector can address. */
|
|
121
|
+
readonly maxPending: number;
|
|
122
|
+
}
|
|
123
|
+
/** Complete plugin configuration; every key has a schema default. */
|
|
124
|
+
interface Config {
|
|
125
|
+
/** Master switch: when false the plugin never claims a request. */
|
|
126
|
+
readonly enabled: boolean;
|
|
127
|
+
/** Session-start default for the runtime `/approval-review on|off` switch. */
|
|
128
|
+
readonly enabledByDefault: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The permission preset that turns this plugin ON. Empty means "always claim",
|
|
131
|
+
* which suits a deployment that has no such preset. When set, the plugin claims
|
|
132
|
+
* nothing unless that preset is the session's active one — that is what makes
|
|
133
|
+
* the access-mode entry the real on/off switch instead of a decorative label.
|
|
134
|
+
*/
|
|
135
|
+
readonly reviewerPreset: string;
|
|
136
|
+
/** Tool-name patterns routed to the reviewer model. */
|
|
137
|
+
readonly reviewTools: string[];
|
|
138
|
+
/** Policy for tools matching no entry in {@link reviewTools}. */
|
|
139
|
+
readonly defaultPolicy: ToolPolicy;
|
|
140
|
+
/** Ordered rules evaluated before the tool table. */
|
|
141
|
+
readonly rules: RiskRuleConfig[];
|
|
142
|
+
/** Reviewer model, prompt, and size limits. */
|
|
143
|
+
readonly reviewer: ReviewerConfig;
|
|
144
|
+
/** Transcript evidence budget. */
|
|
145
|
+
readonly context: ContextConfig;
|
|
146
|
+
/** Highest risk the reviewer may auto-allow. */
|
|
147
|
+
readonly maxAutoAllowRisk: RiskLevel;
|
|
148
|
+
/** Reaction when a verdict's risk exceeds {@link maxAutoAllowRisk}. */
|
|
149
|
+
readonly onRiskExceeded: RiskGateAction;
|
|
150
|
+
/** Reaction when the reviewer reports uncertainty. */
|
|
151
|
+
readonly onUncertain: UncertaintyAction;
|
|
152
|
+
/** Reaction when the reviewer never answered. */
|
|
153
|
+
readonly onReviewerFailure: FallbackAction;
|
|
154
|
+
/** Per-turn reviewer budget. */
|
|
155
|
+
readonly budget: BudgetConfig;
|
|
156
|
+
/** Per-turn reviewer-FAILURE budget, so a broken reviewer cannot retry without bound. */
|
|
157
|
+
readonly maxFailuresPerTurn: number;
|
|
158
|
+
/** Verdict cache settings. */
|
|
159
|
+
readonly verdictCache: VerdictCacheConfig;
|
|
160
|
+
/** Rejection circuit breaker. */
|
|
161
|
+
readonly circuitBreaker: CircuitBreakerConfig;
|
|
162
|
+
/** One-shot `/approve` override. */
|
|
163
|
+
readonly override: OverrideConfig;
|
|
164
|
+
/** Character cap for any reason string this plugin emits. */
|
|
165
|
+
readonly reasonMaxChars: number;
|
|
166
|
+
/** Append the reviewer rationale to the refused tool result the model sees. */
|
|
167
|
+
readonly feedReasonToModel: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Append the reviewer's ALLOW verdict to the accepted tool result.
|
|
170
|
+
*
|
|
171
|
+
* A refusal already carries its rationale into the tool result, which is the
|
|
172
|
+
* only channel this plugin can write durably without inventing a session event
|
|
173
|
+
* type. An allow verdict has no such carrier, so without this the audit ledger
|
|
174
|
+
* can show THAT an action was allowed but never WHY. Turning it on costs one
|
|
175
|
+
* marker block (four short lines) in the model's context per auto-allowed
|
|
176
|
+
* call; turning it off leaves allowed rows rationale-less in the card.
|
|
177
|
+
*/
|
|
178
|
+
readonly recordAllowedVerdicts: boolean;
|
|
179
|
+
/** Language of the `/approval-review` command output. */
|
|
180
|
+
readonly language: 'en' | 'zh';
|
|
181
|
+
}
|
|
182
|
+
/** Schema for {@link Config}; the loader validates against this at mount time. */
|
|
183
|
+
declare const Config: Schema<Config>;
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/audit.d.ts
|
|
186
|
+
/** One rendered audit entry. Every field is plain JSON. */
|
|
187
|
+
interface AuditRecord {
|
|
188
|
+
/** Fresh id minted when the request was reviewed. */
|
|
189
|
+
readonly reviewId: string;
|
|
190
|
+
/** Session-unique sequence for stable ordering in the card. */
|
|
191
|
+
readonly seq: number;
|
|
192
|
+
/** Tool the request was about. */
|
|
193
|
+
readonly toolName: string;
|
|
194
|
+
/** The exact tool call, when the asker supplied one. */
|
|
195
|
+
readonly callId?: string;
|
|
196
|
+
/** Turn the request arrived in. */
|
|
197
|
+
readonly turn: number;
|
|
198
|
+
/** Step the request arrived in. */
|
|
199
|
+
readonly step: number;
|
|
200
|
+
/** Epoch milliseconds when the request was accepted for review. */
|
|
201
|
+
readonly startedAt: number;
|
|
202
|
+
/** Effective tool policy at decision time. */
|
|
203
|
+
readonly policy: 'ai' | 'human' | 'never';
|
|
204
|
+
/** Which rule selected that policy. */
|
|
205
|
+
readonly policySource: string;
|
|
206
|
+
/** The asker's own reason for requesting approval. */
|
|
207
|
+
readonly askReason?: string;
|
|
208
|
+
/** Redaction-free arguments preview (already in the user's own log). */
|
|
209
|
+
readonly argumentsPreview?: string;
|
|
210
|
+
/** How the request was finally resolved, from the host's own decision event. */
|
|
211
|
+
readonly outcome?: 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
212
|
+
/** The reviewer's rationale, folded out of the refused tool result. */
|
|
213
|
+
readonly reason?: string;
|
|
214
|
+
/** Actionable safer alternative the reviewer suggested. */
|
|
215
|
+
readonly suggestion?: string;
|
|
216
|
+
/** Risk grade the reviewer reported. */
|
|
217
|
+
readonly risk?: RiskLevel;
|
|
218
|
+
/** Reviewer route, when it is recorded in the refusal marker. */
|
|
219
|
+
readonly reviewerRoute?: string;
|
|
220
|
+
/** Reviewer duration in milliseconds. */
|
|
221
|
+
readonly durationMs?: number;
|
|
222
|
+
/** Convenience verdict for the card's tone: did this end in a refusal? */
|
|
223
|
+
readonly refused: boolean;
|
|
224
|
+
/** True when the reviewer was uncertain rather than decisive. */
|
|
225
|
+
readonly uncertain: boolean;
|
|
226
|
+
/** True when a one-shot `/approve` override authorized the retry. */
|
|
227
|
+
readonly overridden: boolean;
|
|
228
|
+
}
|
|
229
|
+
/** Client-visible projection value: the ledger for one session. */
|
|
230
|
+
interface AuditView {
|
|
231
|
+
/** Newest-first audit entries. */
|
|
232
|
+
readonly records: readonly AuditRecord[];
|
|
233
|
+
/** Effective auto-review switch (`enabledByDefault` already applied). */
|
|
234
|
+
readonly enabled: boolean;
|
|
235
|
+
/** Reviewer calls already spent in the open turn. */
|
|
236
|
+
readonly reviewsThisTurn: number;
|
|
237
|
+
/** Per-turn reviewer budget in force. */
|
|
238
|
+
readonly maxReviewsPerTurn: number;
|
|
239
|
+
/** Consecutive denials, counting the open turn. */
|
|
240
|
+
readonly consecutiveDenials: number;
|
|
241
|
+
/** Whether the rejection circuit breaker is currently open. */
|
|
242
|
+
readonly circuitOpen: boolean;
|
|
243
|
+
/** Cumulative reviewed-request count for the session. */
|
|
244
|
+
readonly total: number;
|
|
245
|
+
/** Successful refuse count for the session (compact and plain, not a delta). */
|
|
246
|
+
readonly refused: number;
|
|
247
|
+
/** One-shot overrides still usable. */
|
|
248
|
+
readonly pendingOverrides: number;
|
|
249
|
+
/**
|
|
250
|
+
* The reviewer model actually in force for this session: the durable override
|
|
251
|
+
* when one was set, else the deployment default (`''` = inherit the session
|
|
252
|
+
* model).
|
|
253
|
+
*/
|
|
254
|
+
readonly reviewerModel: string;
|
|
255
|
+
/**
|
|
256
|
+
* Provider half of the effective reviewer route (`''` = the deployment
|
|
257
|
+
* default, which itself falls back to the calling agent's provider).
|
|
258
|
+
*/
|
|
259
|
+
readonly reviewerProvider: string;
|
|
260
|
+
}
|
|
261
|
+
/** Raw projection state; the wire view is derived from it. */
|
|
262
|
+
interface AuditState {
|
|
263
|
+
readonly records: readonly AuditRecord[];
|
|
264
|
+
/** In-flight approvals by `approval/asked` id, settled by `approval/decided`. */
|
|
265
|
+
readonly pending: Readonly<Record<string, AuditRecord>>;
|
|
266
|
+
/** `tool/call` arguments by callId, for the preview. */
|
|
267
|
+
readonly arguments: Readonly<Record<string, string>>;
|
|
268
|
+
readonly turn: number;
|
|
269
|
+
readonly step: number;
|
|
270
|
+
readonly enabledOverride?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Durable per-session reviewer ROUTE override, from
|
|
273
|
+
* `/approval-review model [<provider>/]<model>`. Both halves are written
|
|
274
|
+
* together so a cross-provider choice cannot end up asking the session's
|
|
275
|
+
* provider for a model it does not serve.
|
|
276
|
+
*/
|
|
277
|
+
readonly modelOverride?: string;
|
|
278
|
+
/** Provider half of {@link modelOverride}; absent means the configured route. */
|
|
279
|
+
readonly providerOverride?: string;
|
|
280
|
+
readonly reviewsThisTurn: number;
|
|
281
|
+
readonly denialsStreak: number;
|
|
282
|
+
readonly window: readonly boolean[];
|
|
283
|
+
readonly total: number;
|
|
284
|
+
readonly refused: number;
|
|
285
|
+
readonly nextSeq: number;
|
|
286
|
+
readonly pendingOverrides: number;
|
|
287
|
+
}
|
|
288
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
289
|
+
interface SessionProjectionMap {
|
|
290
|
+
/** Audit ledger + live counters for the review card. */
|
|
291
|
+
approvalReview: AuditView;
|
|
292
|
+
}
|
|
293
|
+
interface SessionProjectionStateMap {
|
|
294
|
+
/** Raw fold state behind {@link SessionProjectionMap.approvalReview}. */
|
|
295
|
+
approvalReview: AuditState;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Derive the client-visible ledger from raw state. */
|
|
299
|
+
declare function auditView(state: AuditState, defaults: {
|
|
300
|
+
readonly enabledByDefault: boolean;
|
|
301
|
+
readonly maxReviewsPerTurn: number;
|
|
302
|
+
readonly breakerTrips: boolean;
|
|
303
|
+
readonly defaultReviewerModel: string;
|
|
304
|
+
/** Deployment default provider half; `''` inherits the calling agent's. */
|
|
305
|
+
readonly defaultReviewerProvider: string;
|
|
306
|
+
}): AuditView;
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/index.d.ts
|
|
309
|
+
declare const name = "approval-review";
|
|
310
|
+
/**
|
|
311
|
+
* Consumers: the `/approval-review` command and the LLM seam the reviewer calls.
|
|
312
|
+
* The answerer and the rationale carrier are event listeners, so they need no
|
|
313
|
+
* service injection and stay mounted even if `commands` is absent.
|
|
314
|
+
*/
|
|
315
|
+
declare const inject: string[];
|
|
316
|
+
/** Build the default audit view for a session with no folded state yet. */
|
|
317
|
+
declare function emptyView(config: Config): ReturnType<typeof auditView>;
|
|
318
|
+
/** Register the answerer, the rationale carrier, the command, and the card feed. */
|
|
319
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
320
|
+
//#endregion
|
|
321
|
+
export { Config, type Config as ConfigShape, apply, emptyView, inject, name };
|