// omp-advisor Web client: composer control to pick the advisor model (or turn it off). // // Lives in `conversation.input.right` (a list slot) next to the model seat. The menu lists the // same provider-grouped models as DSH's model picker (the session's model directory), plus // "Off" and "Same as agent". Hovering a model that has reasoning efforts opens a side submenu to // pick its effort; clicking the model row picks it with its default effort. The choice is saved // on the host (POST omp-advisor/settings) and applies from the advisor's next review. window.__ModuleLoader__.load({ id: '@local/omp-advisor', factory(require) { const React = require('react'); const ReactDOM = require('react-dom'); const P = require('@deepseek-ai/dsh-client-ui-primitives'); const h = React.createElement; const NS = 'ompAdvisor'; const en = { label: 'Advisor', off: 'Advisor off', same: 'Same as agent', offItem: 'Off', sameItem: 'Same model as the agent', title: 'Second-model advisor', effort: 'Effort', providerDefault: 'Default', loading: 'Loading models…', error: 'Could not save: {msg}', hint: 'Reviews each step and adds notes or blockers.', }; const zh = { label: '顾问', off: '顾问关闭', same: '同智能体', offItem: '关闭', sameItem: '与智能体相同的模型', title: '第二模型顾问', effort: '推理强度', providerDefault: '默认', loading: '正在加载模型…', error: '保存失败:{msg}', hint: '审查每一步并给出提示或阻断意见。', }; const CSS = ` .oadv-root{position:relative;display:flex;align-items:center;min-width:0} .oadv-trigger{border:0;background:none;color:var(--dsw-alias-label-secondary);height:28px;max-width:200px;padding:0 6px 0 6px;border-radius:var(--dsw-radius-sm);display:flex;align-items:center;gap:5px;font:inherit;font-size:13px;cursor:pointer;min-width:0} .oadv-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)} .oadv-trigger[data-off]{color:var(--dsw-alias-label-caption)} .oadv-trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline:none} .oadv-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} .oadv-effort{color:var(--dsw-alias-label-caption);white-space:nowrap} .oadv-menu,.oadv-sub{position:fixed;z-index:1100;min-width:240px;max-width:min(380px,100vw - 32px);max-height:min(420px,100vh - 96px);overflow-y:auto;padding:4px;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-prominent);border-radius:var(--dsw-radius-lg,10px);background:var(--dsw-alias-bg-l1,var(--dsw-alias-surface-l1,#1e1e1e))} .oadv-sub{min-width:160px} .oadv-head{padding:6px 8px 4px;font-size:12px;color:var(--dsw-alias-label-tertiary)} .oadv-group{padding:8px 8px 2px;font-size:11px;letter-spacing:.02em;color:var(--dsw-alias-label-caption);text-transform:uppercase} .oadv-item{width:100%;display:flex;align-items:center;gap:8px;min-height:32px;padding:0 8px;border:0;background:none;color:inherit;font:inherit;font-size:13px;text-align:left;border-radius:var(--dsw-radius-md);cursor:pointer} .oadv-item:hover,.oadv-item[data-active]{background:var(--dsw-alias-interactive-bg-hover)} .oadv-item-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .oadv-item-meta{color:var(--dsw-alias-label-caption);font-size:12px;white-space:nowrap} .oadv-check{width:14px;flex:none;display:inline-flex;justify-content:center} .oadv-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.2))} .oadv-status{padding:8px;font-size:12px;color:var(--dsw-alias-label-tertiary)} .oadv-err{padding:6px 8px;font-size:12px;color:var(--dsw-alias-state-danger,#f85149)} `; let cssDone = false; function installCss() { if (cssDone || typeof document === 'undefined') return; const s = document.createElement('style'); s.dataset.ompAdvisor = ''; s.textContent = CSS; document.head.appendChild(s); cssDone = true; } const Ic = (name) => P[name] || (() => null); const IconCheck = Ic('IconCheckOutlineRegular'); const IconChevronDown = Ic('IconChevronDownOutlineRegular'); const IconChevronRight = Ic('IconChevronRightOutlineRegular'); const IconAdvisor = P.IconSparkleRegular || P.IconEyeOutlineRegular || (() => null); const fill = (s, v) => String(s).replace(/\{(\w+)\}/g, (_, k) => (v && v[k] !== undefined ? v[k] : '')); const effortName = (e) => (e?.id === 'xhigh' ? 'Extra' : e?.name ?? e?.id); // ---- host settings store (shared by every session's control) ---- const settings = (() => { let value = null; let inflight = null; const subs = new Set(); const emit = () => subs.forEach((f) => f()); async function call(init) { const res = await fetch('omp-advisor/settings', { ...init, headers: { accept: 'application/json', ...(init?.body ? { 'content-type': 'application/json' } : {}) } }); let json = {}; try { json = await res.json(); } catch { /* empty */ } if (!res.ok) throw new Error(json.message || res.statusText || String(res.status)); return json; } return { subscribe(f) { subs.add(f); return () => subs.delete(f); }, get() { return value; }, load() { inflight ??= call().then((v) => { value = v; emit(); }).catch(() => {}).finally(() => { inflight = null; }); return inflight; }, async save(patch) { const prev = value; value = { ...(value ?? {}), ...patch }; emit(); try { value = await call({ method: 'POST', body: JSON.stringify(patch) }); emit(); } catch (e) { value = prev; emit(); throw e; } }, }; })(); function useSettings() { const v = React.useSyncExternalStore(settings.subscribe, settings.get); React.useEffect(() => { if (v === null) settings.load(); }, [v]); return v; } function place(anchor, menu, { side = 'above', gap = 6 } = {}) { const a = anchor.getBoundingClientRect(); const m = menu.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; if (side === 'right') { let left = a.right + 2; if (left + m.width > vw - 8) left = a.left - m.width - 2; let top = Math.min(a.top - 4, vh - m.height - 8); return { left: Math.max(8, left), top: Math.max(8, top) }; } let left = Math.min(a.left, vw - m.width - 8); let top = a.top - m.height - gap; if (top < 8) top = a.bottom + gap; return { left: Math.max(8, left), top: Math.max(8, Math.min(top, vh - m.height - 8)) }; } function AdvisorPicker({ directory, load, t }) { React.useEffect(installCss, []); const st = useSettings(); const dir = React.useSyncExternalStore((f) => directory.subscribe(f), () => directory.getSnapshot()); const [open, setOpen] = React.useState(false); const [hover, setHover] = React.useState(null); // { key, el, model, group } const [err, setErr] = React.useState(null); const [pos, setPos] = React.useState(null); const [subPos, setSubPos] = React.useState(null); const rootRef = React.useRef(null); const trigRef = React.useRef(null); const menuRef = React.useRef(null); const subRef = React.useRef(null); const hoverTimer = React.useRef(null); const enabled = st?.enabled !== false; const own = !!(st?.provider && st?.model); const groups = dir?.groups ?? []; const current = own ? groups.find((g) => g.id === st.provider)?.models.find((m) => m.id === st.model) : null; const effortOf = (model, id) => effortName(model?.reasoning?.efforts?.find((e) => e.id === id)) ?? id; const label = !st ? t('label') : !enabled ? t('off') : own ? (current?.name ?? st.model) : t('same'); const effortLabel = enabled && own && current?.reasoning ? (st.reasoningEffort ? effortOf(current, st.reasoningEffort) : t('providerDefault')) : null; // open / close behaviour React.useEffect(() => { if (!open) return undefined; if (dir?.status !== 'ready') load(); const onDown = (e) => { for (const r of [rootRef, menuRef, subRef]) if (r.current?.contains(e.target)) return; setOpen(false); }; const onKey = (e) => { if (e.key === 'Escape') { setOpen(false); trigRef.current?.focus(); } }; document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [open]); React.useLayoutEffect(() => { if (!open) { setPos(null); setHover(null); return; } if (menuRef.current && trigRef.current) setPos(place(trigRef.current, menuRef.current)); }, [open, groups.length, dir?.status]); React.useLayoutEffect(() => { if (!hover) { setSubPos(null); return; } if (subRef.current && hover.el) setSubPos(place(hover.el, subRef.current, { side: 'right' })); }, [hover]); const choose = async (patch) => { setErr(null); setOpen(false); try { await settings.save(patch); } catch (e) { setErr(String(e.message || e)); setOpen(true); } }; const pickModel = (group, model, effort) => choose({ enabled: true, provider: group.id, model: model.id, reasoningEffort: effort === undefined ? (model.reasoning?.defaultEffort ?? null) : effort, }); const enterRow = (group, model, el) => { clearTimeout(hoverTimer.current); if (!model.reasoning?.efforts?.length) { setHover(null); return; } hoverTimer.current = setTimeout(() => setHover({ key: `${group.id}/${model.id}`, el, group, model }), 120); }; const leaveRow = () => { clearTimeout(hoverTimer.current); }; const check = (on) => h('span', { className: 'oadv-check' }, on ? h(IconCheck, { size: 14 }) : null); const menu = open ? ReactDOM.createPortal(h('div', { ref: menuRef, className: 'oadv-menu', role: 'menu', 'aria-label': t('title'), style: pos ? { left: pos.left, top: pos.top } : { left: -9999, top: -9999, visibility: 'hidden' }, onMouseLeave: () => { clearTimeout(hoverTimer.current); }, }, h('div', { className: 'oadv-head' }, t('title'), ' · ', t('hint')), err ? h('div', { className: 'oadv-err' }, fill(t('error'), { msg: err })) : null, h('button', { type: 'button', role: 'menuitemradio', 'aria-checked': !enabled, className: 'oadv-item', onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: false }) }, check(!enabled), h('span', { className: 'oadv-item-name' }, t('offItem'))), h('button', { type: 'button', role: 'menuitemradio', 'aria-checked': enabled && !own, className: 'oadv-item', onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: true, provider: '', model: '', reasoningEffort: null }) }, check(enabled && !own), h('span', { className: 'oadv-item-name' }, t('sameItem'))), h('div', { className: 'oadv-sep' }), dir?.status === 'loading' && !groups.length ? h('div', { className: 'oadv-status' }, t('loading')) : null, ...groups.flatMap((group) => [ h('div', { key: `g:${group.id}`, className: 'oadv-group' }, group.name ?? group.id), ...group.models.map((model) => { const key = `${group.id}/${model.id}`; const selected = enabled && own && st.provider === group.id && st.model === model.id; const hasEfforts = !!model.reasoning?.efforts?.length; return h('button', { key, type: 'button', role: 'menuitemradio', 'aria-checked': selected, 'aria-haspopup': hasEfforts ? 'menu' : undefined, className: 'oadv-item', 'data-active': hover?.key === key ? '' : undefined, onMouseEnter: (e) => enterRow(group, model, e.currentTarget), onMouseLeave: leaveRow, onFocus: (e) => enterRow(group, model, e.currentTarget), onKeyDown: (e) => { if (e.key === 'ArrowRight' && hasEfforts) { setHover({ key, el: e.currentTarget, group, model }); setTimeout(() => subRef.current?.querySelector('button')?.focus(), 0); } }, onClick: () => pickModel(group, model), disabled: model.available === false, }, check(selected), h('span', { className: 'oadv-item-name' }, model.name ?? model.id), selected && st.reasoningEffort ? h('span', { className: 'oadv-item-meta' }, effortOf(model, st.reasoningEffort)) : null, hasEfforts ? h(IconChevronRight, { size: 12 }) : null); }), ])), document.body) : null; const sub = open && hover ? ReactDOM.createPortal(h('div', { ref: subRef, className: 'oadv-sub', role: 'menu', 'aria-label': t('effort'), style: subPos ? { left: subPos.left, top: subPos.top } : { left: -9999, top: -9999, visibility: 'hidden' }, onMouseEnter: () => clearTimeout(hoverTimer.current), onKeyDown: (e) => { if (e.key === 'ArrowLeft') { hover.el?.focus(); setHover(null); } }, }, h('div', { className: 'oadv-head' }, hover.model.name ?? hover.model.id, ' · ', t('effort')), ...(hover.model.reasoning.defaultEffort === undefined ? [h('button', { key: 'def', type: 'button', role: 'menuitemradio', className: 'oadv-item', 'aria-checked': enabled && own && st.provider === hover.group.id && st.model === hover.model.id && !st.reasoningEffort, onClick: () => pickModel(hover.group, hover.model, null), }, check(enabled && own && st.provider === hover.group.id && st.model === hover.model.id && !st.reasoningEffort), h('span', { className: 'oadv-item-name' }, t('providerDefault')))] : []), ...hover.model.reasoning.efforts.map((e) => { const on = enabled && own && st.provider === hover.group.id && st.model === hover.model.id && (st.reasoningEffort ?? hover.model.reasoning.defaultEffort) === e.id; return h('button', { key: e.id, type: 'button', role: 'menuitemradio', 'aria-checked': on, className: 'oadv-item', onClick: () => pickModel(hover.group, hover.model, e.id) }, check(on), h('span', { className: 'oadv-item-name' }, effortName(e)), e.id === hover.model.reasoning.defaultEffort ? h('span', { className: 'oadv-item-meta' }, t('providerDefault')) : null); })), document.body) : null; return h('div', { ref: rootRef, className: 'oadv-root' }, h('button', { ref: trigRef, type: 'button', className: 'oadv-trigger', 'data-off': enabled ? undefined : '', 'aria-haspopup': 'menu', 'aria-expanded': open, title: t('title'), onClick: () => setOpen((v) => !v), }, h(IconAdvisor, { size: 14 }), h('span', { className: 'oadv-label' }, label), effortLabel ? h('span', { className: 'oadv-effort' }, effortLabel) : null, h(IconChevronDown, { size: 12 })), menu, sub); } return { inject: ['slots', 'locale', 'sessions', 'modelDirectories'], apply(ctx) { ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'omp-advisor: dictionaries'); ctx.slots.inject('conversation.input.right', () => ctx.slots.register({ name: 'conversation.input.right', id: 'omp-advisor', order: 50, locale: NS, inject: (sessionId) => { const directory = ctx.modelDirectories.directoryFor(sessionId); // Read-only use of the directory: the advisor never calls directory.select (that would // switch the agent's own model). return { directory: directory.store, load: () => { directory.load().catch(() => {}); } }; }, }, AdvisorPicker)); }, }; }, });