feat(settings): drop the assistant/device/data/app entries (TODO 16, 17)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 08:18:54 +08:00
1 parent bde041c12d
commit 401926d7eb
3 files changed
+228 -12

No files matched your search

+21 -9
View File
@@ -350,19 +350,31 @@
### 16. 移除助理与设备连接 ### 16. 移除助理与设备连接
- [ ] 移除设置中的“助理”“设备连接”导航和页面入口。 - [x] 移除设置中的“助理”“设备连接”导航和页面入口。
- [ ] 清理默认选中、内部导航、深链接/旧配置回退中的无效目标。 - [x] 清理默认选中、内部导航、深链接/旧配置回退中的无效目标。
- [ ] 验收:设置中不再出现这两项,旧导航目标不会导致空白页。 - [x] 验收:设置中不再出现这两项,旧导航目标不会导致空白页。
状态:待实施;范围仅为用户指定的设置入口,不据此删除其他模块功能。 状态:已实现并通过真实窗口回归。
原版证据:WorkBuddy 5.8.3,REA Evidence `ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`(归档 `.tmp/rea-assessment-settings-window/layout.json`,视口 900×680):设置导航 16 项 / 4 组、条目 `190×32`、x=12、y 从 111 起;`.settings-modal__nav` 200 宽而条目 190 宽,10px 差值为内容溢出时 thin 滚动条占用的槽位。观察范围:设置窗口导航栏;未覆盖:被移除项的原版内部页面(本轮不迁移其内容)。
实现:`src/renderer/src/components/SettingsDialog.tsx` 新增 `REMOVED_SETTINGS_TABS = ['claw','deviceConnections','dataManagement','buddyApps']`、导出 `VISIBLE_SETTINGS_TABS = SOURCE_WINDOWS_SETTINGS_TABS.filter(...)`(12 项)、`normalizeSection()`;`?section=` 启动、已打开窗口的 `onSettingsNavigate` 深链接统一走 normalize(`models`→`model`,已移除项→`model` 回退),`SourceSettingsNavigation` 新增 `visibleTabs` 属性按可见列表渲染。未删除任何用户数据、已安装内容或配置。
验证:`scripts/verify-settings-nav.cjs` 在真实隔离 Electron(真实 preload/IPC/renderer + 原生设置窗口)中 19 项断言全部通过:12 项顺序与移除项不在 DOM/标签/搜索元数据、条目几何 x=12 / 32 高 / 宽度等于导航内容宽、3 个组标题 + 1 条 about 分隔线、未接入项 `aria-disabled` + 「暂未接入」提示且点击提示「该设置尚未接入」、`appearance`/`models` 仍可打开、`openSettings('claw')`(旧窗口导航)与 `openSettings('dataManagement')`(新窗口 `?section=` 启动)都回退到模型面板、无渲染器报错;报告 `.tmp/settings-nav-5YlLX6/report.json`。回归:`npm run typecheck`、`npm run build`、`npm run test:chat`、`npm run test:conversation`、`npm run test:conversation-ipc`、`node scripts/verify-composer-context-info.cjs` 全部通过。
遗留:移除 4 项后本窗口导航不再溢出,条目宽 200(原版 16 项溢出时为 190);未补 `scrollbar-gutter`,因为 REA 回收的原版 CSS 没有该规则,保持源码级一致,条目增多再次溢出时会自动回到 190。
### 17. 移除数据管理与应用管理 ### 17. 移除数据管理与应用管理
- [ ] 移除设置中的“数据管理”“应用管理”导航和页面入口。 - [x] 移除设置中的“数据管理”“应用管理”导航和页面入口。
- [ ] 清理相关无效导航与页面占位。 - [x] 清理相关无效导航与页面占位。
- [ ] 验收:入口完全移除且其余设置导航正常;不删除已有用户数据或已安装内容。 - [x] 验收:入口完全移除且其余设置导航正常;不删除已有用户数据或已安装内容。
状态:待实施。 状态:已实现并通过真实窗口回归(与 16 同一次改动,代码见上)。
原版证据、实现、验证:同 16。`dataManagement`、`buddyApps` 与 `claw`、`deviceConnections` 一并进入 `REMOVED_SETTINGS_TABS`,走同一份 normalize 回退与 `visibleTabs` 渲染;验收脚本同时断言这两项(以及 `claw`、`deviceConnections`)不在 DOM、标签和搜索元数据中,且 `openSettings('dataManagement')` 深链接落到模型面板而不是空白页。未触碰本地会话数据、安装中心与任何已有用户内容。
遗留:无新增;数据管理/应用管理的原版页面内容不再复刻(用户指定仅移除入口)。
### 18. 安装中心 ### 18. 安装中心
@@ -443,7 +455,7 @@
## 推进顺序 ## 推进顺序
1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。 1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。
2. 设置:先落实 16、17 的入口范围,再依次推进 10 → 11 → 12 → 13 → 15 → 19。 2. 设置:16、17 的入口范围已落实并验证;接着依次推进 10 → 11 → 12 → 13 → 15 → 19。
3. 14、18、20 保留为待拆解,后续单独展开。 3. 14、18、20 保留为待拆解,后续单独展开。
## 每项交付记录模板 ## 每项交付记录模板
+185
View File
@@ -0,0 +1,185 @@
// TODO 16/17 验收:移除设置里的「助理 / 设备连接 / 数据管理 / 应用管理」入口,并保证旧导航目标不落空白页。
// 真实隔离 YCode(真实 preload/IPC/渲染进程)+ 原生设置窗口。
//
// 原版依据:REA `ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`
// (WorkBuddy 5.8.3 设置窗口,viewport 记录于 .tmp/rea-assessment-settings-window/layout.json):
// 设置导航共 16 项、分 4 组(general 5 / feature 6 / dataSecurity 3 / about 2),
// 条目几何 190×32、x=12、y=111 起、组间距 76。
// 用户要求(TODO 16/17)在其中移除:助理(claw)、设备连接(deviceConnections)、数据管理(dataManagement)、
// 应用管理(buddyApps);其余条目保持原样(未接入的仍为禁用 + 「暂未接入」)。
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
const EXPECTED_TABS = ['settings', 'account', 'subscriptionUsage', 'appearance', 'keyboardShortcuts', 'personalization',
'memory', 'extensions', 'models', 'securityCenter', 'helpFeedback', 'getHelp']
const REMOVED_TABS = ['claw', 'deviceConnections', 'dataManagement', 'buddyApps']
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/settings-nav-'))
const data = path.join(directory, 'data')
const home = path.join(directory, 'home')
await fs.mkdir(data, { recursive: true })
await fs.mkdir(home, { recursive: true })
const now = Date.now()
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions: [{ id: 'settings-nav-session', title: 'Settings Nav',
cwd: home, createdAt: now, updatedAt: now + 1, messages: [{ id: 'settings-nav-user', role: 'user', content: '设置导航验收', createdAt: now, status: 'done' }] }] }))
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
YCODE_DISABLE_GPU: '1' }
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
const child = require('node:child_process').spawn(require('electron'),
['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('SETTINGS_NAV_')) process.stdout.write(bytes) })
child.stderr.on('data', bytes => { output += bytes })
const timer = setTimeout(() => child.kill(), 240000)
child.once('exit', async code => {
clearTimeout(timer)
await fs.writeFile(path.join(directory, 'process.log'), output)
console.log(JSON.stringify({ directory, exitCode: code }))
if (code) console.error(output.slice(-4000))
process.exitCode = code ? 1 : 0
})
})().catch(error => { console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const { pathToFileURL } = require('node:url')
const directory = process.env.YCODE_QA_DIRECTORY
app.setPath('home', path.join(directory, 'home'))
const checks = [], snapshots = {}, rendererErrors = []
let main, settings, phase = 'boot'
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
async function read(source, win = main) {
let timer
try {
return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => {
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000)
})])
} finally { clearTimeout(timer) }
}
async function wait(source, label, win = main) {
for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) }
throw Error('Timeout: ' + label)
}
async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) }
function check(label, actual, expected) {
const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected)
checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected })
}
async function openSettings(section) {
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
for (let index = 0; index < 200; index++) {
settings = BrowserWindow.getAllWindows().find(win => win !== main)
if (settings && !settings.webContents.isLoading()) break
await pause(25)
}
assert.ok(settings, 'settings window')
await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings)
await pause(300)
return settings
}
const navSource = `(()=>{const items=[...document.querySelectorAll('.settings-navigation__item')];
const nav=document.querySelector('.settings-modal__nav');
return {tabs:items.map(node=>node.dataset.settingsTab),labels:items.map(node=>node.textContent.trim()),
disabled:items.map(node=>node.getAttribute('aria-disabled')),titles:items.map(node=>node.getAttribute('title')),
groups:[...document.querySelectorAll('.settings-navigation__group-title')].map(node=>node.textContent.trim()),
dividers:document.querySelectorAll('.settings-navigation__group-divider').length,
nav:[nav?Math.round(nav.getBoundingClientRect().width):0,nav?nav.clientWidth:0,nav?nav.scrollHeight-nav.clientHeight:0],
geometry:items.map(node=>{const r=node.getBoundingClientRect();return [Math.round(r.x),Math.round(r.width),Math.round(r.height)]})}})()`
;(async () => {
// 导入前不做任何 await,避免 Electron 先进入 ready(主进程 bundle 顶层会调用 disableHardwareAcceleration)。
const sync = require('node:fs')
sync.mkdirSync(process.env.YCODE_USER_DATA_DIR, { recursive: true })
app.setPath('userData', process.env.YCODE_USER_DATA_DIR)
app.setPath('sessionData', process.env.YCODE_USER_DATA_DIR)
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
await app.whenReady()
for (let index = 0; index < 200; index++) {
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
if (main && !main.webContents.isLoading()) break
await pause(40)
}
assert.ok(main, 'main window')
main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
await wait('!!window.ycode', 'main renderer')
await pause(1200)
await step('settings-nav-entries')
await openSettings('model')
settings.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
const nav = await read(navSource, settings)
snapshots.nav = nav
check('settings nav keeps the 12 remaining entries in order', nav.tabs, EXPECTED_TABS)
check('removed entries are gone from the DOM', REMOVED_TABS.map(tab => nav.tabs.includes(tab)), [false, false, false, false])
check('removed entries are gone from labels and search metadata', [nav.labels.join('|').includes('助理'), nav.labels.join('|').includes('设备连接'),
nav.labels.join('|').includes('数据管理'), nav.labels.join('|').includes('应用管理')], [false, false, false, false])
// 原版 16 项在 680 高窗口溢出,thin 滚动条占 10px,故条目实测 190 = 200 - 10;
// 移除 4 项后本窗口不再溢出,条目宽度应等于导航栏内容宽(原版 CSS 无 scrollbar-gutter)。
const navContentWidth = nav.nav[1]
check('nav keeps the recovered 32px entry geometry at x=12 and fills the nav content width',
[nav.geometry.every(([x, width, height]) => x === 12 && height === 32), nav.geometry.every(([, width]) => width === navContentWidth), nav.nav[2] === 0], [true, true, true])
check('nav width reflects the recovered 200px column with a thin scrollbar only when overflowing',
[nav.nav[0] === 200 || nav.nav[0] === 190, nav.nav[2] === 0 ? navContentWidth === 200 : navContentWidth === 190], [true, true])
// 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。
check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1])
const expectedDisabled = EXPECTED_TABS.filter(tab => tab !== 'models' && tab !== 'appearance')
check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled)
check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length)
check('models and appearance stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['appearance', 'models'])
await step('settings-search-excludes-removed')
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
if(!input)throw new Error('missing search');Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input,'助理');
input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`, settings)
await pause(240)
snapshots.search = await read(`(()=>{const results=[...document.querySelectorAll('.settings-navigation__item')];
return {tabs:results.map(node=>node.dataset.settingsTab),empty:!!document.querySelector('.source-settings-search-empty')}})()`, settings)
check('searching the removed 助理 entry finds nothing', [snapshots.search.tabs, snapshots.search.empty], [[], true])
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input,'');input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`, settings)
await pause(200)
await step('settings-navigation-still-works')
await read(`(()=>{const node=document.querySelector('[data-settings-tab="appearance"]');node.click();return true})()`, settings)
await wait(`!!document.querySelector('.segmented')`, 'appearance panel', settings)
check('appearance entry still opens its panel', await read(`!!document.querySelector('.segmented')&&!document.querySelector('.models-settings-panel')`, settings), true)
await read(`(()=>{const node=document.querySelector('[data-settings-tab="models"]');node.click();return true})()`, settings)
await wait(`!!document.querySelector('.models-settings-panel')`, 'models panel', settings)
check('models entry still opens its panel', await read(`!!document.querySelector('.models-settings-panel')`, settings), true)
await step('stale-navigation-falls-back')
await read(`(()=>{const node=document.querySelector('[data-settings-tab="securityCenter"]');node.click();return true})()`, settings)
await pause(200)
snapshots.unwiredClick = await read(`({alert:(document.querySelector('.settings-modal__panel [role="alert"]')||{}).textContent||'',models:!!document.querySelector('.models-settings-panel')})`, settings)
check('unwired entry reports 该设置尚未接入 instead of a blank page', snapshots.unwiredClick.alert.includes('该设置尚未接入'), true)
// 旧深链接指向已移除入口:应回退到模型设置。
await read(`window.ycode.app.openSettings('claw')`)
await pause(400)
check('stale navigation to 助理 falls back to the models panel', await read(`!!document.querySelector('.models-settings-panel')`, settings), true)
const previous = settings.id
await read(`document.querySelector('.settings-window-close').click()`, settings)
for (let index = 0; index < 200 && !settings.isDestroyed(); index++) await pause(20)
check('settings window closed before the deep-link relaunch', settings.isDestroyed(), true)
// 新窗口直接以已移除的 section 启动:同样不能是空白页。
await openSettings('dataManagement')
check('fresh window with a removed section lands on the models panel', await read(`!!document.querySelector('.models-settings-panel')`, settings), true)
check('fresh window reuses a new settings window', settings.id !== previous, true)
const finalNav = await read(navSource, settings)
snapshots.finalNav = finalNav
check('nav still excludes removed entries after relaunch', finalNav.tabs, EXPECTED_TABS)
check('no renderer error logs', rendererErrors, [])
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL',
scope: 'Real isolated YCode App/preload/IPC/renderer + native settings window; navigation entries, geometry, search, disabled hints and stale-target fallback measured live.',
sourceEvidence: ['ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e'], snapshots, checks, rendererErrors }
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
console.log('SETTINGS_NAV_' + report.result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('SETTINGS_NAV_FAILURE ' + JSON.stringify(failure))
app.exit(report.result === 'PASS' ? 0 : 1)
})().catch(async error => {
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, snapshots, rendererErrors }, null, 2))
console.error(error); app.exit(1)
})
}
+22 -3
View File
@@ -3,6 +3,7 @@ import { useSettingsStore } from '../stores/settings'
import ModelSettingsPanel from './ModelSettingsPanel' import ModelSettingsPanel from './ModelSettingsPanel'
import SourceSettingsNavigation from './SourceSettingsNavigation' import SourceSettingsNavigation from './SourceSettingsNavigation'
import { useSharedTheme } from '../useSharedTheme' import { useSharedTheme } from '../useSharedTheme'
import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation'
import './settings-shell-source.css' import './settings-shell-source.css'
interface Props { interface Props {
@@ -12,15 +13,33 @@ interface Props {
nativeWindow?: boolean nativeWindow?: boolean
} }
/**
* 用户指定的移除项(TODO 16/17):助理、设备连接、数据管理、应用管理。
* 原版 5.8.3 设置导航是 16 项(REA `ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`,
* 8-9-13:111/145/179/213/247 · 323/357/391/425/459/493 · 569/603/637 · 694/728),
* 这里按用户要求只移除上述四项,其余保留(未接入的仍按原样显示为禁用+「暂未接入」)。
*/
const REMOVED_SETTINGS_TABS: readonly string[] = ['claw', 'deviceConnections', 'dataManagement', 'buddyApps']
/** 设置页可见导航(12 项)。 */
export const VISIBLE_SETTINGS_TABS = SOURCE_WINDOWS_SETTINGS_TABS.filter((tab) => !REMOVED_SETTINGS_TABS.includes(tab))
/** 旧导航/深链接可能指向已移除入口:回退到模型设置,避免落到空白页。 */
function normalizeSection(value: string | null | undefined): string {
if (!value) return 'model'
const normalized = value === 'models' ? 'model' : value
return REMOVED_SETTINGS_TABS.includes(normalized) ? 'model' : normalized
}
export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWindow = false }: Props): React.JSX.Element { export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWindow = false }: Props): React.JSX.Element {
const settings = useSettingsStore() const settings = useSettingsStore()
const [section, setSection] = useState(new URLSearchParams(location.search).get('section') ?? 'model') const [section, setSection] = useState(() => normalizeSection(new URLSearchParams(location.search).get('section')))
const [createRequest, setCreateRequest] = useState(() => new URLSearchParams(location.search).get('action') === 'new' ? 1 : 0) const [createRequest, setCreateRequest] = useState(() => new URLSearchParams(location.search).get('action') === 'new' ? 1 : 0)
const [error, setError] = useState('') const [error, setError] = useState('')
useEffect(() => { useEffect(() => {
if (!nativeWindow) return if (!nativeWindow) return
return window.ycode.app.onSettingsNavigate?.((next, action) => { return window.ycode.app.onSettingsNavigate?.((next, action) => {
setError(''); setSection(next === 'models' ? 'model' : next) setError(''); setSection(normalizeSection(next))
if (action === 'new') setCreateRequest(value => value + 1) if (action === 'new') setCreateRequest(value => value + 1)
}) })
}, [nativeWindow]) }, [nativeWindow])
@@ -32,7 +51,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
return () => window.removeEventListener('keydown', escape) return () => window.removeEventListener('keydown', escape)
}, [onClose]) }, [onClose])
const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}> const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}>
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['models', 'appearance']} <div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['models', 'appearance']} visibleTabs={VISIBLE_SETTINGS_TABS}
onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div> onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div>
<div className="settings-modal__content"><div className="settings-modal__panel"> <div className="settings-modal__content"><div className="settings-modal__panel">
{error && <p role="alert">{error}</p>} {error && <p role="alert">{error}</p>}