fix: align supervisor settings layout and default model naming

Group settings tabs and forms in a single surface with consistent spacing. Rename the model section to cover model selection and execution limits, and use Default model for the default connection option.

Release note: 修复监督者设置页签与表单贴边的问题,统一设置布局,并将默认模型选项名称改为默认模型。
This commit is contained in:
mesalogo
2026-10-03 23:43:58 +08:00
parent 352da66dbd
commit 0e9c9d4927
6 changed files with 99 additions and 10 deletions
@@ -44,9 +44,11 @@
没有自动运行、报告或关联建议时,只展示计划配置和已有计划,不显示零值统计、空趋势、建议、报告或审计区。手动回顾不计入心跳统计;产生自动运行或报告后展示对应结果区。
2026-09-23 当前已实现“模型超时与并发”表单:心跳报告与监督者整理分别设置,均为 30 至 600 的整数秒,默认 240;监督模型并发数为 1 至 4 的整数,默认 1。保存真实字段 `heartbeatReportTimeoutSeconds`、`supervisorOrganizeTimeoutSeconds`、`supervisorModelConcurrency`;无效输入不能提交,设置加载/保存中或锁定时禁用,失败保留草稿并提供重试。超时以阶段读取设置的时点为准,不改变已冻结的值;并发数影响后续派发,降低上限不打断在途请求。
设置的四个分类共用一个设置面板,使用共享表面、边框和卡片圆角。面板内边距为 `--space-4`(16px),分段页签按内容宽度靠左,与下方表单间隔 `--space-6`(24px);页签和表单标题左对齐。表单不再另设边框、底色或内边距,说明段落不叠加浏览器默认外边距。页签及设置面板不参与 Flex 高度压缩,窄窗口页签在自身内部横向滚动,长表单随页面纵向滚动。
并发说明明确报告和监督整理共用上限,排队不计入模型超时;普通聊天不受此设置限制,但仍与监督竞争上游额度。表单不宣称提供商全局并发、RPM、TPM 或 Retry-After 限流已实现。
模型分类的标题为“模型与运行限制 / Model and execution limits”,包含监督者模型选择、整理超时与并发。整理超时为 30 至 600 的整数秒,默认 240;监督模型并发数为 1 至 4 的整数,默认 1。保存字段为 `supervisorModelProfileId`、`supervisorOrganizeTimeoutSeconds`、`supervisorModelConcurrency`;心跳不再单独调用报告模型,此处不显示报告超时。无效输入不能提交,设置加载/保存中或锁定时禁用,失败保留草稿并提供重试。超时以阶段读取设置的时点为准,不改变已冻结的值;并发数影响后续派发,降低上限不打断在途请求。
并发说明明确监督模型调用共用上限,排队不计入模型超时;普通聊天不受此设置限制,但仍与监督竞争上游额度。表单不宣称提供商全局并发、RPM、TPM 或 Retry-After 限流已实现。
模型超时不包含数据收集与保存,直连模型另有独立 600 秒传输超时。“回顾算法”表单提供“每次读取来源条数”“每批文本量”“每批消息数”和“单次响应容量”,显示默认值及合法范围,使用“保存回顾算法”独立提交。文本量内部仍按 UTF-16 单元计算,界面不要求用户理解编码单位。保存经 App 的现有设置回调进入共享设置存储,加载/保存/锁定时禁用,失败保留草稿。取值以[当前设置合同](./review-scheduling-design.md#当前设置合同)为准。帮助说明回顾持续处理至完成,每批保存进度,可从活动记录主动暂停并继续;已保存配置用“同时处理任务数”解释并发,说明实际并发还受当前监督设置限制。
@@ -123,6 +125,8 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
设置 `GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT=1` 验证工作回顾、空态、活动、待处理及失败场景,在 2000、1440、1024、390px 和浅深主题下共 40 个组合,检查全宽、空态居中及位于工具栏下方、阶段连接线和长内容溢出。
设置 `GOODBUDDY_SUPERVISOR_SETTINGS=1` 只验证设置布局:四个分类分别覆盖浅深主题及 1440×1100、1024×768、390×480,共 24 个组合。检查五个主页签、四个设置页签、单一可见面板、16px 内边距、24px 内容间距、无嵌套表单边框及横向溢出,并保存截图与几何测量;使用生产组件和模拟设置,不调用模型。
设置 `GOODBUDDY_SUPERVISOR_PLAN_MODAL=1` 可只验证五页签 fixture 中的计划 Modal:创建和编辑各覆盖浅深主题、1440×1100、390×1100、390×480,检查关闭按钮、底部按钮对齐、正文滚动、鼠标命中和原生键盘操作。截图及测量写入同一产物目录。
设置 `GOODBUDDY_SUPERVISOR_RECAP=1` 可验证本次工作回顾分区:1440×1100 和 390×1100 下覆盖监督结果与自动报告均有内容、仅有监督结果、两者均空,另检查历史结果运行中仍可阅读以及活动入口。该分支检查每页一处刷新、自动报告和建议的归属、正文段落与宽度,并截取自动页建议、展开报告和审计区块。
+3 -3
View File
@@ -579,10 +579,10 @@ describe('HeartbeatCenter', () => {
expect(screen.queryByRole('heading', { name })).not.toBeInTheDocument()
}
fireEvent.click(screen.getByRole('tab', { name: '设置' }))
expect(screen.getByRole('heading', { level: 2, name: '模型超时与并发' })).toBeInTheDocument()
expect(screen.getByRole('heading', { level: 2, name: '模型与运行限制' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('tab', { name: '回顾整理' }))
expect(screen.getByRole('heading', { level: 2, name: '回顾算法' })).toBeInTheDocument()
expect(screen.queryByRole('heading', { name: '模型超时与并发' })).not.toBeInTheDocument()
expect(screen.queryByRole('heading', { name: '模型与运行限制' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('tab', { name: '故事与经验' }))
expect(screen.getByRole('switch', { name: '跨项目关联' })).toBeVisible()
fireEvent.click(screen.getByRole('tab', { name: '建议' }))
@@ -691,7 +691,7 @@ describe('HeartbeatCenter', () => {
expect(screen.queryByRole('region', { name: '心跳计划' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('tab', { name: '设置' }))
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
expect(screen.getByRole('heading', { name: '模型超时与并发' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: '模型与运行限制' })).toBeInTheDocument()
})
it('keeps an unsaved draft when switching settings tabs', () => {
@@ -44,10 +44,10 @@ export const heartbeat = {
},
timeouts: {
model: 'Model',
followDefault: 'Follow app default',
followDefault: 'Default model',
unavailableModel: 'Selected model is unavailable',
modelHelp: 'Uses an existing text model for reviews, summaries, stories, experiences, and suggestions. Saving affects the next execution, including continue or retry; a review already running keeps its model.',
title: 'Model timeouts and concurrency',
title: 'Model and execution limits',
report: 'Heartbeat report timeout (seconds)',
organize: 'Supervisor organize timeout (seconds)',
help: 'The organize timeout defaults to 240 seconds, adjustable from 30 to 600. Reviews save it at creation and retain it when continued; suggestions use the same limit.',
@@ -41,10 +41,10 @@ export const heartbeat = {
},
timeouts: {
model: '模型',
followDefault: '跟随应用默认',
followDefault: '默认模型',
unavailableModel: '所选模型已不可用',
modelHelp: '复用已有文本模型,用于回顾、汇总、故事、经验和建议。保存后从下次执行生效,包括继续或重试;正在运行的回顾保持原模型。',
title: '模型超时与并发',
title: '模型与运行限制',
report: '心跳报告超时(秒)',
organize: '监督者整理超时(秒)',
help: '整理超时默认 240 秒,可设置为 30 至 600 的整数秒。回顾在创建时保存整理超时,继续时沿用;生成建议使用同一上限。',
+31 -1
View File
@@ -1116,6 +1116,36 @@
gap: var(--space-3);
}
.supervisor-settings {
display: flex;
flex: 0 0 auto;
min-width: 0;
flex-direction: column;
gap: var(--space-6);
padding: var(--space-4);
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
background: var(--surface-raised);
}
.supervisor-settings > .page-tabs {
align-self: flex-start;
}
.supervisor-settings .heartbeat-settings__editor {
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
gap: var(--space-4);
overflow-wrap: anywhere;
}
.supervisor-settings .heartbeat-settings__editor > p {
margin: 0;
color: var(--text-secondary);
}
.supervisor-center__sections .heartbeat-settings__editor > h2 {
margin: 0;
font-size: var(--font-section-title);
@@ -1326,4 +1356,4 @@
margin: var(--space-1) 0 0;
padding-inline-start: 1.1em;
color: var(--text-muted);
}
}
@@ -84,6 +84,61 @@ app
assert(report.scrollWidth <= report.clientWidth && report.disclosures === 0, 'No overflow or redundant disclosure')
return report
}
if (process.env.GOODBUDDY_SUPERVISOR_SETTINGS) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL)
await wait('!!document.querySelector("#supervisor-tab-settings")')
await js('document.querySelector("#supervisor-tab-settings").click()')
await wait('!!document.querySelector(".supervisor-settings")')
await js('document.fonts.ready')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme = '${theme}'`)
for (const [width, height] of [[1440, 1100], [1024, 768], [390, 480]]) {
win.setContentSize(width, height)
for (const tab of ['model', 'review', 'stories', 'suggestions']) {
await js(`document.querySelector('#supervisor-settings-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
await wait(`document.querySelector('#supervisor-settings-tab-${tab}').getAttribute('aria-selected') === 'true' && innerWidth === ${width}`)
await settle()
const report = await js(`(() => {
const root = document.querySelector('.supervisor-settings');
const nav = root.querySelector('.page-tabs');
const panel = root.querySelector('[role=tabpanel]:not([hidden])');
const form = panel.querySelector('form');
const box = e => { const r = e.getBoundingClientRect(); return {left:r.left, right:r.right, top:r.top, bottom:r.bottom, height:r.height}; };
return { root:box(root), nav:box(nav), form:box(form), heading:form.querySelector('h2').textContent,
primaryTabs:document.querySelectorAll('.heartbeat-center > .page-tabs [role=tab]').length,
settingsTabs:nav.querySelectorAll('[role=tab]').length,
visiblePanels:root.querySelectorAll('[role=tabpanel]:not([hidden])').length,
navFlex:getComputedStyle(nav).flexShrink, rootFlex:getComputedStyle(root).flexShrink,
formBorder:getComputedStyle(form).borderTopWidth, formBackground:getComputedStyle(form).backgroundColor,
pageWidth:document.documentElement.scrollWidth, rootWidth:root.clientWidth, rootScrollWidth:root.scrollWidth,
controls:[...form.querySelectorAll('input,select,button')].map(box) };
})()`)
assert.equal(report.primaryTabs, 5)
assert.equal(report.settingsTabs, 4)
assert.equal(report.visiblePanels, 1)
assert.equal(report.nav.top - report.root.top, 17, 'Tabs inset from the card border')
assert.equal(report.form.top - report.nav.bottom, 24, 'Tabs separated from section content')
assert.equal(report.form.left, report.nav.left, 'Section and tabs share the content edge')
assert.equal(report.navFlex, '0')
assert.equal(report.rootFlex, '0')
assert(report.nav.height >= 36, 'Short windows do not compress tabs')
assert.equal(report.formBorder, '0px', 'No nested form border')
assert.equal(report.formBackground, 'rgba(0, 0, 0, 0)', 'One settings surface')
assert(report.pageWidth <= width && report.rootScrollWidth <= report.rootWidth, 'No horizontal page or card overflow')
assert(report.controls.every(control => control.left >= report.form.left && control.right <= report.form.right + 1), 'Fields fit the section')
if (tab === 'model') assert.equal(report.heading, '模型与运行限制')
reports.push({ theme, width, height, tab, ...report })
await writeFile(join(artifacts, `settings-${theme}-${width}-${tab}.png`), (await win.webContents.capturePage()).toPNG())
}
}
}
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'settings-measurements.json'), JSON.stringify(reports, null, 2))
console.log(JSON.stringify({ settingsCases: reports.length, tabInset: 17, contentGap: 24, errors }))
win.destroy()
app.quit()
return
}
if (process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT) {
win.show()
const controlsOnly = process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT === 'controls'