v1.1.1: advisor popup matches the model picker (MenuSurface/MenuGroup, rows, brand icons, tokens)

This commit is contained in:
Agent committed 2026-10-11 00:54:36 +00:00
1 parent 31319ea7ae
commit f85f445ef6
2 files changed
+85 -60

No files matched your search

+84 -59
View File
@@ -27,26 +27,35 @@ window.__ModuleLoader__.load({
hint: '审查每一步并给出提示或阻断意见。', hint: '审查每一步并给出提示或阻断意见。',
}; };
// Mirrors model-selector-plus (`.msp-*`) — same surface, rows, type scale and tokens — so the
// advisor popup reads as a sibling of the model picker in every theme.
const CSS = ` const CSS = `
.oadv-root{position:relative;display:flex;align-items:center;min-width:0} .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{border-radius:var(--dsw-radius-sm);min-width:0;max-width:min(240px,30cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:0 4px 0 8px;font-size:13px;font-weight:400;line-height:20px;display:flex}
.oadv-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)} .oadv-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}
.oadv-trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}
.oadv-trigger[data-off]{color:var(--dsw-alias-label-caption)} .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{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
.oadv-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} .oadv-effort{color:var(--dsw-alias-label-caption);white-space:nowrap;flex-shrink:1000}
.oadv-effort{color:var(--dsw-alias-label-caption);white-space:nowrap} .oadv-chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}
.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-chevron-open{transform:rotate(180deg)}
.oadv-sub{min-width:160px} .oadv-icon{flex:none;display:inline-block;line-height:1}
.oadv-head{padding:6px 8px 4px;font-size:12px;color:var(--dsw-alias-label-tertiary)} .oadv-menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(260px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(420px,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}
.oadv-group{padding:8px 8px 2px;font-size:11px;letter-spacing:.02em;color:var(--dsw-alias-label-caption);text-transform:uppercase} .oadv-sub{min-width:min(180px,100vw - 32px)}
.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-groups{min-height:0;overflow-y:auto}
.oadv-item:hover,.oadv-item[data-active]{background:var(--dsw-alias-interactive-bg-hover)} .oadv-option{box-sizing:border-box;width:100%;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:8px;min-height:34px;padding:5px 7px;display:flex;border-radius:var(--dsw-radius-md);font:inherit}
.oadv-item-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .oadv-option:hover,.oadv-option:focus-visible,.oadv-option[data-active]{background:var(--dsw-alias-interactive-bg-hover)}
.oadv-item-meta{color:var(--dsw-alias-label-caption);font-size:12px;white-space:nowrap} .oadv-option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
.oadv-check{width:14px;flex:none;display:inline-flex;justify-content:center} .oadv-copy{flex-direction:column;flex:1;min-width:0;display:flex}
.oadv-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.2))} .oadv-name{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:400;line-height:18px;overflow:hidden}
.oadv-status{padding:8px;font-size:12px;color:var(--dsw-alias-label-tertiary)} .oadv-sub-text{color:var(--dsw-alias-label-caption);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:14px;overflow:hidden}
.oadv-err{padding:6px 8px;font-size:12px;color:var(--dsw-alias-state-danger,#f85149)} .oadv-meta{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}
.oadv-check{color:var(--dsw-alias-label-primary);flex:0 0 14px;place-items:center;display:grid}
.oadv-check svg{width:14px;height:14px}
.oadv-more{width:12px;height:12px;color:var(--dsw-alias-menu-icon,var(--dsw-alias-label-caption));flex:none}
.oadv-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1)}
.oadv-status{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:12px;line-height:18px}
.oadv-err{border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);margin-bottom:3px;padding:6px 7px;font-size:11px;line-height:16px}
`; `;
let cssDone = false; let cssDone = false;
function installCss() { function installCss() {
@@ -63,6 +72,15 @@ window.__ModuleLoader__.load({
const IconChevronDown = Ic('IconChevronDownOutlineRegular'); const IconChevronDown = Ic('IconChevronDownOutlineRegular');
const IconChevronRight = Ic('IconChevronRightOutlineRegular'); const IconChevronRight = Ic('IconChevronRightOutlineRegular');
const IconAdvisor = P.IconSparkleRegular || P.IconEyeOutlineRegular || (() => null); const IconAdvisor = P.IconSparkleRegular || P.IconEyeOutlineRegular || (() => null);
// Shared surfaces from DSH primitives; plain divs if a build lacks them.
const Surface = P.MenuSurface || React.forwardRef((p, ref) => h('div', { ...p, ref }));
const Group = P.MenuGroup || (({ label, children }) => h('div', null, h('div', { className: 'oadv-status' }, label), children));
/** Brand icon from model-selector-plus when it is mounted; an empty 16px slot otherwise. */
function Brand({ group, model }) {
const b = window.__dshModelBrands;
if (b) return h(b.BrandIcon, { brand: b.brandOf(group.id, group.name, model.id, model.name) });
return h('span', { className: 'oadv-icon', style: { width: 16, height: 16 }, 'aria-hidden': true });
}
const fill = (s, v) => String(s).replace(/\{(\w+)\}/g, (_, k) => (v && v[k] !== undefined ? v[k] : '')); 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); const effortName = (e) => (e?.id === 'xhigh' ? 'Extra' : e?.name ?? e?.id);
@@ -183,74 +201,81 @@ window.__ModuleLoader__.load({
}; };
const leaveRow = () => { clearTimeout(hoverTimer.current); }; const leaveRow = () => { clearTimeout(hoverTimer.current); };
const check = (on) => h('span', { className: 'oadv-check' }, on ? h(IconCheck, { size: 14 }) : null); const check = (on) => h('span', { className: 'oadv-check' }, on ? h(IconCheck) : null);
const hidden = { left: 0, top: 0, visibility: 'hidden' };
const isSel = (group, model) => enabled && own && st.provider === group.id && st.model === model.id;
const menu = open ? ReactDOM.createPortal(h('div', { /** One row in the model picker's shape: icon · name (+ caption) · meta · check · chevron. */
const option = ({ key, checked, icon, name, caption, meta, more, active, ...rest }) => h('button', {
key, type: 'button', role: 'menuitemradio', 'aria-checked': checked, className: 'oadv-option',
'data-active': active ? '' : undefined, ...rest,
},
icon ?? null,
h('span', { className: 'oadv-copy' },
h('span', { className: 'oadv-name' }, name),
caption ? h('span', { className: 'oadv-sub-text' }, caption) : null),
meta ? h('span', { className: 'oadv-meta' }, meta) : null,
check(checked),
more ? h(IconChevronRight, { className: 'oadv-more' }) : null);
const menu = open ? ReactDOM.createPortal(h(Surface, {
ref: menuRef, className: 'oadv-menu', role: 'menu', 'aria-label': t('title'), 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' }, style: pos ? { left: pos.left, top: pos.top } : hidden,
onMouseLeave: () => { clearTimeout(hoverTimer.current); }, onMouseLeave: () => { clearTimeout(hoverTimer.current); },
}, },
h('div', { className: 'oadv-head' }, t('title'), ' · ', t('hint')), err ? h('div', { className: 'oadv-err', role: 'alert' }, fill(t('error'), { msg: err })) : null,
err ? h('div', { className: 'oadv-err' }, fill(t('error'), { msg: err })) : null, h('div', { className: 'oadv-groups' },
h('button', { type: 'button', role: 'menuitemradio', 'aria-checked': !enabled, className: 'oadv-item', onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: false }) }, h(Group, { label: t('title') },
check(!enabled), h('span', { className: 'oadv-item-name' }, t('offItem'))), option({ key: 'off', checked: !enabled, icon: h(IconAdvisor, { size: 16 }), name: t('offItem'), caption: t('hint'), onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: false }) }),
h('button', { type: 'button', role: 'menuitemradio', 'aria-checked': enabled && !own, className: 'oadv-item', onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: true, provider: '', model: '', reasoningEffort: null }) }, option({ key: 'same', checked: enabled && !own, icon: h('span', { className: 'oadv-icon', style: { width: 16, height: 16 } }), name: t('sameItem'), onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: true, provider: '', model: '', reasoningEffort: null }) })),
check(enabled && !own), h('span', { className: 'oadv-item-name' }, t('sameItem'))), dir?.status === 'loading' && !groups.length ? h('div', { className: 'oadv-status', role: 'status' }, t('loading')) : null,
h('div', { className: 'oadv-sep' }), ...groups.map((group) => h(Group, { key: `g:${group.id}`, label: group.id === 'deepseek-account' ? 'DeepSeek' : (group.name ?? group.id) },
dir?.status === 'loading' && !groups.length ? h('div', { className: 'oadv-status' }, t('loading')) : null, group.models.map((model) => {
...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 key = `${group.id}/${model.id}`;
const selected = enabled && own && st.provider === group.id && st.model === model.id; const selected = isSel(group, model);
const hasEfforts = !!model.reasoning?.efforts?.length; const hasEfforts = !!model.reasoning?.efforts?.length;
return h('button', { return option({
key, type: 'button', role: 'menuitemradio', 'aria-checked': selected, 'aria-haspopup': hasEfforts ? 'menu' : undefined, key, checked: selected, icon: h(Brand, { group, model }), name: model.name ?? model.id,
className: 'oadv-item', 'data-active': hover?.key === key ? '' : undefined, meta: selected && st.reasoningEffort ? effortOf(model, st.reasoningEffort) : null,
more: hasEfforts, active: hover?.key === key,
'aria-haspopup': hasEfforts ? 'menu' : undefined, disabled: model.available === false,
onMouseEnter: (e) => enterRow(group, model, e.currentTarget), onMouseEnter: (e) => enterRow(group, model, e.currentTarget),
onMouseLeave: leaveRow, onMouseLeave: leaveRow,
onFocus: (e) => enterRow(group, model, e.currentTarget), 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); } }, 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), onClick: () => pickModel(group, model),
disabled: model.available === false, });
}, }))))), document.body) : null;
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', { const sub = open && hover ? ReactDOM.createPortal(h(Surface, {
ref: subRef, className: 'oadv-sub', role: 'menu', 'aria-label': t('effort'), ref: subRef, className: 'oadv-menu oadv-sub', role: 'menu', 'aria-label': t('effort'),
style: subPos ? { left: subPos.left, top: subPos.top } : { left: -9999, top: -9999, visibility: 'hidden' }, style: subPos ? { left: subPos.left, top: subPos.top } : hidden,
onMouseEnter: () => clearTimeout(hoverTimer.current), onMouseEnter: () => clearTimeout(hoverTimer.current),
onKeyDown: (e) => { if (e.key === 'ArrowLeft') { hover.el?.focus(); setHover(null); } }, onKeyDown: (e) => { if (e.key === 'ArrowLeft') { hover.el?.focus(); setHover(null); } },
}, },
h('div', { className: 'oadv-head' }, hover.model.name ?? hover.model.id, ' · ', t('effort')), h(Group, { label: `${hover.model.name ?? hover.model.id} · ${t('effort')}` },
...(hover.model.reasoning.defaultEffort === undefined ? [h('button', { ...(hover.model.reasoning.defaultEffort === undefined ? [option({
key: 'def', type: 'button', role: 'menuitemradio', className: 'oadv-item', key: 'def', checked: isSel(hover.group, hover.model) && !st.reasoningEffort, name: t('providerDefault'),
'aria-checked': enabled && own && st.provider === hover.group.id && st.model === hover.model.id && !st.reasoningEffort,
onClick: () => pickModel(hover.group, hover.model, null), 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) => { ...hover.model.reasoning.efforts.map((e) => option({
const on = enabled && own && st.provider === hover.group.id && st.model === hover.model.id key: e.id, name: effortName(e),
&& (st.reasoningEffort ?? hover.model.reasoning.defaultEffort) === e.id; checked: isSel(hover.group, hover.model) && (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) }, meta: e.id === hover.model.reasoning.defaultEffort ? t('providerDefault') : null,
check(on), h('span', { className: 'oadv-item-name' }, effortName(e)), onClick: () => pickModel(hover.group, hover.model, e.id),
e.id === hover.model.reasoning.defaultEffort ? h('span', { className: 'oadv-item-meta' }, t('providerDefault')) : null); })))), document.body) : null;
})), document.body) : null;
const triggerIcon = enabled && own && current ? h(Brand, { group: groups.find((g) => g.id === st.provider), model: current }) : h(IconAdvisor, { size: 16 });
return h('div', { ref: rootRef, className: 'oadv-root' }, return h('div', { ref: rootRef, className: 'oadv-root' },
h('button', { h('button', {
ref: trigRef, type: 'button', className: 'oadv-trigger', 'data-off': enabled ? undefined : '', ref: trigRef, type: 'button', className: 'oadv-trigger', 'data-off': enabled ? undefined : '',
'aria-haspopup': 'menu', 'aria-expanded': open, title: t('title'), 'aria-haspopup': 'menu', 'aria-expanded': open, title: t('title'),
onClick: () => setOpen((v) => !v), onClick: () => setOpen((v) => !v),
}, },
h(IconAdvisor, { size: 14 }), triggerIcon,
h('span', { className: 'oadv-label' }, label), h('span', { className: 'oadv-label' }, label),
effortLabel ? h('span', { className: 'oadv-effort' }, effortLabel) : null, effortLabel ? h('span', { className: 'oadv-effort' }, effortLabel) : null,
h(IconChevronDown, { size: 12 })), h(IconChevronDown, { className: open ? 'oadv-chevron oadv-chevron-open' : 'oadv-chevron' })),
menu, sub); menu, sub);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@local/omp-advisor", "name": "@local/omp-advisor",
"version": "1.1.0", "version": "1.1.1",
"description": "oh-my-pi style advisor for DSH: a second model reviews every agent step and injects notes, concerns or blockers.", "description": "oh-my-pi style advisor for DSH: a second model reviews every agent step and injects notes, concerns or blockers.",
"private": true, "private": true,
"type": "module", "type": "module",