From 0e9c9d4927b51fae3cf2e524ccc562cd1fbedfb2 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sat, 3 Oct 2026 23:43:58 +0800 Subject: [PATCH] fix: align supervisor settings layout and default model naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: 修复监督者设置页签与表单贴边的问题,统一设置布局,并将默认模型选项名称改为默认模型。 --- .../conversation-supervision/ui-design.md | 8 ++- src/renderer/src/HeartbeatCenter.test.tsx | 6 +- .../src/i18n/locales/en-US/heartbeat.ts | 4 +- .../src/i18n/locales/zh-CN/heartbeat.ts | 4 +- src/renderer/src/supervisor-workspace.css | 32 ++++++++++- tests/support/supervisor-layout-driver.mjs | 55 +++++++++++++++++++ 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/docs/features/conversation-supervision/ui-design.md b/docs/features/conversation-supervision/ui-design.md index 5904e63e..02fa54fe 100644 --- a/docs/features/conversation-supervision/ui-design.md +++ b/docs/features/conversation-supervision/ui-design.md @@ -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 下覆盖监督结果与自动报告均有内容、仅有监督结果、两者均空,另检查历史结果运行中仍可阅读以及活动入口。该分支检查每页一处刷新、自动报告和建议的归属、正文段落与宽度,并截取自动页建议、展开报告和审计区块。 diff --git a/src/renderer/src/HeartbeatCenter.test.tsx b/src/renderer/src/HeartbeatCenter.test.tsx index 8dfed5ce..2ace0399 100644 --- a/src/renderer/src/HeartbeatCenter.test.tsx +++ b/src/renderer/src/HeartbeatCenter.test.tsx @@ -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', () => { diff --git a/src/renderer/src/i18n/locales/en-US/heartbeat.ts b/src/renderer/src/i18n/locales/en-US/heartbeat.ts index 9cc2d8e4..7ecc56b4 100644 --- a/src/renderer/src/i18n/locales/en-US/heartbeat.ts +++ b/src/renderer/src/i18n/locales/en-US/heartbeat.ts @@ -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.', diff --git a/src/renderer/src/i18n/locales/zh-CN/heartbeat.ts b/src/renderer/src/i18n/locales/zh-CN/heartbeat.ts index f70465bd..c22c365b 100644 --- a/src/renderer/src/i18n/locales/zh-CN/heartbeat.ts +++ b/src/renderer/src/i18n/locales/zh-CN/heartbeat.ts @@ -41,10 +41,10 @@ export const heartbeat = { }, timeouts: { model: '模型', - followDefault: '跟随应用默认', + followDefault: '默认模型', unavailableModel: '所选模型已不可用', modelHelp: '复用已有文本模型,用于回顾、汇总、故事、经验和建议。保存后从下次执行生效,包括继续或重试;正在运行的回顾保持原模型。', - title: '模型超时与并发', + title: '模型与运行限制', report: '心跳报告超时(秒)', organize: '监督者整理超时(秒)', help: '整理超时默认 240 秒,可设置为 30 至 600 的整数秒。回顾在创建时保存整理超时,继续时沿用;生成建议使用同一上限。', diff --git a/src/renderer/src/supervisor-workspace.css b/src/renderer/src/supervisor-workspace.css index 9ac305d1..a02992fb 100644 --- a/src/renderer/src/supervisor-workspace.css +++ b/src/renderer/src/supervisor-workspace.css @@ -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); -} \ No newline at end of file +} diff --git a/tests/support/supervisor-layout-driver.mjs b/tests/support/supervisor-layout-driver.mjs index 6b48b2e6..274cb21b 100644 --- a/tests/support/supervisor-layout-driver.mjs +++ b/tests/support/supervisor-layout-driver.mjs @@ -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'