diff --git a/apps/desktop/e2e/accessibility-coverage.spec.ts b/apps/desktop/e2e/accessibility-coverage.spec.ts index e04e7ddd66..884bba0052 100644 --- a/apps/desktop/e2e/accessibility-coverage.spec.ts +++ b/apps/desktop/e2e/accessibility-coverage.spec.ts @@ -142,18 +142,21 @@ test('module pages and global overlays expose named actionable controls', async await page.keyboard.press('Escape'); }); -test('data-backed conversation exposes ordered todos and keyboard access to tools, models, and Graph', async ({ +test('data-backed conversation exposes an open workbar face and keyboard access to tools, models, and Graph', async ({ accessibilityNarrativeWindow: page, }) => { const cdp = await page.context().newCDPSession(page); await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible(); - const todoRegion = page.getByRole('region', { name: '任务待办' }); - await expect(todoRegion).toBeVisible(); - await expect(todoRegion.getByRole('listitem')).toHaveText([ - '补齐桌面端无障碍覆盖', - '核对模型选择器的键盘路径', - '确认工具结果可以展开阅读', - ]); + // The scenario opens a face beside the transcript so the AX sweep below sees + // the workbar's own tree, not an empty column. Which face is incidental — it + // was the Task face until that face was retired — so this asserts the strip + // reports one, rather than reaching into the face's contents. + const workbar = page.getByRole('complementary', { name: '任务工作栏' }); + await expect(workbar).toBeVisible(); + await expect(workbar.getByRole('tab', { name: '变更' })).toHaveAttribute( + 'aria-selected', + 'true', + ); await assertAxHealth(cdp, 'conversation/data-backed'); await expect(page.getByRole('main')).toHaveCount(1); @@ -277,20 +280,29 @@ test('composer and workbar entry points expose named actionable controls', async await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible(); await assertAxHealth(cdp, 'workbar/launcher'); - const workbarPanels = [ - '侧边对话', - '变更', - '终端', - '浏览器', - '生成文件', - '待办', - '追踪', - ] as const; - for (const panel of workbarPanels) { - await page - .getByRole('list', { name: '打开工具' }) - .getByRole('button', { name: new RegExp(`^${panel}(?: |$)`) }) - .click(); + // Registry order, minus the Task face this shell retired. The launcher is + // the empty state, so it opens the first face and then goes away; every face + // after it is opened from the [+] menu, which is the only entry point once a + // face is on screen. The faces stay open behind one another — the strip is a + // `tablist` now, so each one has to keep reporting its own tab. + const workbarPanels = ['侧边对话', '变更', '终端', '浏览器', '生成文件', '追踪'] as const; + for (const [index, panel] of workbarPanels.entries()) { + if (index === 0) { + await page + .getByRole('list', { name: '打开工具' }) + .getByRole('button', { name: new RegExp(`^${panel}(?: |$)`) }) + .click(); + } else { + await page.getByRole('button', { name: '打开或关闭工作栏的面' }).first().click(); + const faceMenu = page.getByRole('menu'); + await expect(faceMenu).toBeVisible(); + await assertAxHealth(cdp, `workbar/face-menu/${panel}`); + await faceMenu.getByRole('menuitem', { name: panel, exact: true }).click(); + // The menu stays open on select, so that one pick can close a face as + // well as open one. Dismiss it before reading the panel underneath. + await page.keyboard.press('Escape'); + await expect(faceMenu).toBeHidden(); + } const activeTab = page.getByRole('tab', { name: new RegExp(panel) }); await expect(activeTab).toBeVisible(); await expect(activeTab).toHaveAttribute('aria-selected', 'true'); @@ -298,14 +310,8 @@ test('composer and workbar entry points expose named actionable controls', async await expect(page.getByRole('region', { name: '任务终端' })).toBeVisible(); } else if (panel === '浏览器') { await expect(page.getByRole('region', { name: '嵌入式浏览器' })).toBeVisible(); - } else if (panel === '待办') { - await expect(page.getByRole('region', { name: '任务待办' })).toBeVisible(); } await assertAxHealth(cdp, `workbar/${panel}`); - if (panel !== workbarPanels.at(-1)) { - await page.getByRole('button', { name: '打开工作栏标签' }).first().click(); - await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible(); - } } }); diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 42ce777aca..c62b9d4ae0 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -799,8 +799,8 @@ export const test = base.extend({ showWindow: true, }, use); }, - // A data-backed conversation with settled tool evidence and a populated - // task ledger. Shown because the accessibility journey follows real native + // A data-backed conversation with settled tool evidence and the workbar open + // beside it. Shown because the accessibility journey follows real native // focus order through the transcript into the composer controls. accessibilityNarrativeWindow: async ({}, use) => { await withE2eWindow({ diff --git a/apps/desktop/e2e/session-workbar.spec.ts b/apps/desktop/e2e/session-workbar.spec.ts index ae6b329e54..22a6ae701d 100644 --- a/apps/desktop/e2e/session-workbar.spec.ts +++ b/apps/desktop/e2e/session-workbar.spec.ts @@ -324,9 +324,16 @@ test('titlebar workbar action restores an existing tool instead of the picker', const safeAreaToggleBox = await collapseButton.boundingBox(); expect(safeAreaToggleBox).not.toBeNull(); - await page.getByRole('button', { name: '打开工作栏标签' }).click(); + // [+] is a menu over the panel now, not a swap to the launcher: the face you + // are reading stays on screen while you pick another one. + await page.getByRole('button', { name: '打开或关闭工作栏的面' }).click(); + const faceMenu = page.getByRole('menu'); + await expect(faceMenu).toBeVisible(); + await expect(panel).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(faceMenu).toBeHidden(); const picker = page.getByRole('list', { name: '打开工具' }); - await expect(picker).toBeVisible(); + await expect(picker).not.toBeVisible(); await collapseButton.click(); const expandButton = workspaceActions.getByRole('button', { name: '展开任务工作栏' }); @@ -440,19 +447,22 @@ test('Side Chat survives collapse, confirms close, and cleans up on source switc await page.getByRole('button', { name: '展开任务工作栏' }).click(); await expect(companion).toBeVisible(); - const workbarToolbar = page.getByRole('toolbar', { name: '任务工作栏标签' }).first(); - const closeActiveSideChat = () => - workbarToolbar - .getByRole('tab', { selected: true }) - .locator('..') - .getByRole('button', { name: /^关闭/ }); - await closeActiveSideChat().click(); + // Closing is the same [+] menu that opens: the face already on screen carries + // a checkmark, and picking it again asks to close it. + const closeActiveSideChat = async () => { + await page.getByRole('button', { name: '打开或关闭工作栏的面' }).first().click(); + await page + .getByRole('menu') + .getByRole('menuitem', { name: '侧边对话', exact: true }) + .click(); + }; + await closeActiveSideChat(); const confirmation = page.getByRole('dialog'); await expect(confirmation).toContainText('这个临时侧边对话会被永久删除'); await confirmation.getByRole('button', { name: '取消' }).click(); await expect(companion).toBeVisible(); - await closeActiveSideChat().click(); + await closeActiveSideChat(); await confirmation.getByRole('button', { name: '关闭侧边对话' }).click(); await expect(companion).toHaveCount(0); await expect diff --git a/apps/desktop/e2e/settings.spec.ts b/apps/desktop/e2e/settings.spec.ts index c7a421ef23..c54dfdbe0f 100644 --- a/apps/desktop/e2e/settings.spec.ts +++ b/apps/desktop/e2e/settings.spec.ts @@ -123,13 +123,15 @@ test('settings hides expanded workbar chrome and restores it on close', async ({ const workbar = page.locator('.maka-session-workbar[data-placement="right"]'); const workbarToolbar = workbar.getByRole('toolbar', { name: '任务工作栏标签' }); await expect(workbarToolbar).toBeVisible(); - await expect(workbarToolbar.getByRole('button', { name: '打开工作栏标签' })).toBeVisible(); + await expect( + workbarToolbar.getByRole('button', { name: '打开或关闭工作栏的面' }), + ).toBeVisible(); await expect(workbarToolbar.getByRole('button', { name: '收起任务工作栏' })).toBeVisible(); await page - .getByRole('button', { name: /待办.*查看和维护这个任务的待办台账/ }) + .getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }) .click(); - const taskTab = workbarToolbar.getByRole('tab', { name: '待办' }); - await expect(taskTab).toBeVisible(); + const openFaceTab = workbarToolbar.getByRole('tab', { name: '变更' }); + await expect(openFaceTab).toBeVisible(); await ensureSidebarExpanded(page); await page.getByRole('button', { name: '设置' }).click(); @@ -138,7 +140,7 @@ test('settings hides expanded workbar chrome and restores it on close', async ({ await page.keyboard.press('Escape'); await expect(workbarToolbar).toBeVisible(); - await expect(taskTab).toBeVisible(); + await expect(openFaceTab).toBeVisible(); }); test('wide settings gutters scroll the whole main pane', async ({ window: page }) => { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0c07196fe2..ac41d0e15e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -73,8 +73,6 @@ "@astryxdesign/core": "0.5.2", "@astryxdesign/theme-neutral": "0.5.2", "@babel/parser": "7.29.7", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", "@maka/ui": "0.1.0", @@ -102,8 +100,6 @@ "@ant-design/icons-svg", "@astryxdesign/core", "@astryxdesign/theme-neutral", - "@dnd-kit/core", - "@dnd-kit/sortable", "@fontsource-variable/geist", "@fontsource-variable/geist-mono", "@maka/ui", diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 4570aa0895..06a7759a64 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -450,7 +450,7 @@ "./theme": 1 }, "importSpecifiers": 1, - "nonTriviaTokens": 672 + "nonTriviaTokens": 666 }, "src/renderer/app-shell-effects.ts": { "importDeclarations": 12, diff --git a/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt b/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt index ff0943aa45..373f18d046 100644 --- a/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt +++ b/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt @@ -813,126 +813,6 @@ THE SOFTWARE. ================================================================================ -Package: @dnd-kit/accessibility@3.1.1 -Declared license: MIT -Selected license: MIT -Repository: git+https://github.com/clauderic/dnd-kit.git#packages/accessibility - ---- LICENSE --- -MIT License - -Copyright (c) 2021, Claudéric Demers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -================================================================================ - -Package: @dnd-kit/core@6.3.1 -Declared license: MIT -Selected license: MIT -Repository: git+https://github.com/clauderic/dnd-kit.git#packages/core - ---- LICENSE --- -MIT License - -Copyright (c) 2021, Claudéric Demers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -================================================================================ - -Package: @dnd-kit/sortable@10.0.0 -Declared license: MIT -Selected license: MIT -Repository: git+https://github.com/clauderic/dnd-kit.git#packages/sortable - ---- LICENSE --- -MIT License - -Copyright (c) 2021, Claudéric Demers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -================================================================================ - -Package: @dnd-kit/utilities@3.2.2 -Declared license: MIT -Selected license: MIT -Repository: git+https://github.com/clauderic/dnd-kit.git#packages/utilities - ---- LICENSE --- -MIT License - -Copyright (c) 2021, Claudéric Demers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -================================================================================ - Package: @formatjs/fast-memoize@3.1.7 Declared license: MIT Selected license: MIT @@ -13739,27 +13619,6 @@ SOFTWARE. ================================================================================ -Package: tslib@2.8.1 -Declared license: 0BSD -Selected license: 0BSD -Repository: https://github.com/Microsoft/tslib.git - ---- LICENSE.txt --- -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - -================================================================================ - Package: tuf-js@6.0.0 Declared license: MIT Selected license: MIT diff --git a/apps/desktop/src/main/__tests__/use-session-todo.test.ts b/apps/desktop/src/main/__tests__/use-session-todo.test.ts deleted file mode 100644 index ca05257f6a..0000000000 --- a/apps/desktop/src/main/__tests__/use-session-todo.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { strict as assert } from 'node:assert'; -import { afterEach, describe, it } from 'node:test'; -import { act, createElement } from 'react'; -import type { SessionTodoItem } from '@maka/core/session-todo'; -import type { UiLocale } from '@maka/ui'; -import { cleanupFakeDom, installReactRenderer } from './fake-dom.js'; -import { - createFakeWorkbarServices, - useSessionTodo, - WorkbarServicesProvider, - type WorkbarServices, -} from '../../renderer/features/workbar/testing.js'; - -/** - * Regression coverage for the Traditional Chinese error-copy leak - * (2026-09-02 review): this hook used to route zh-CN and zh-TW through the - * same hard-coded Simplified helper, so a zh-TW reader saw 请求超时 instead - * of 請求逾時. It now goes through the shared, locale-aware classifier like - * every sibling surface (use-session-trace.ts, session-review-panel.tsx, - * session-terminal-panel.tsx, artifact-pane.tsx). - */ - -function Probe(props: { - services: WorkbarServices; - sessionId?: string; - locale: UiLocale; - onSnapshot: (snapshot: ReturnType) => void; -}) { - return createElement( - WorkbarServicesProvider, - { services: props.services }, - createElement(TodoProbe, props), - ); -} - -function TodoProbe(props: { - sessionId?: string; - locale: UiLocale; - onSnapshot: (snapshot: ReturnType) => void; -}) { - const snapshot = useSessionTodo(props.sessionId, { - locale: props.locale, - loadFailed: 'load failed', - }); - props.onSnapshot(snapshot); - return null; -} - -function createHarness(read: () => Promise): WorkbarServices { - return createFakeWorkbarServices({ - todo: { - read, - subscribeChanges: () => () => undefined, - }, - }); -} - -describe('useSessionTodo', () => { - afterEach(() => { - cleanupFakeDom(); - delete (globalThis as { window?: unknown }).window; - }); - - it('renders the Traditional Chinese category copy for a zh-TW reader, not Simplified', async () => { - const { root } = installReactRenderer(); - const services = createHarness(async () => { - throw new Error('request timeout'); - }); - let snapshot: ReturnType | undefined; - - await act(async () => { - root.render( - createElement(Probe, { - services, - sessionId: 'session-1', - locale: 'zh-TW', - onSnapshot: (value) => { - snapshot = value; - }, - }), - ); - }); - - assert.equal(snapshot?.error, '請求逾時', 'a zh-TW reader must not see Simplified 请求超时'); - }); - - it('still renders the Simplified Chinese category copy for a zh-CN reader', async () => { - const { root } = installReactRenderer(); - const services = createHarness(async () => { - throw new Error('request timeout'); - }); - let snapshot: ReturnType | undefined; - - await act(async () => { - root.render( - createElement(Probe, { - services, - sessionId: 'session-1', - locale: 'zh-CN', - onSnapshot: (value) => { - snapshot = value; - }, - }), - ); - }); - - assert.equal(snapshot?.error, '请求超时'); - }); - - it('falls back to the caller-supplied copy for an unclassified error in English', async () => { - const { root } = installReactRenderer(); - const services = createHarness(async () => { - throw new Error('unexpected boom'); - }); - let snapshot: ReturnType | undefined; - - await act(async () => { - root.render( - createElement(Probe, { - services, - sessionId: 'session-1', - locale: 'en', - onSnapshot: (value) => { - snapshot = value; - }, - }), - ); - }); - - assert.equal(snapshot?.error, 'load failed'); - }); -}); diff --git a/apps/desktop/src/main/__tests__/workbar-model.test.ts b/apps/desktop/src/main/__tests__/workbar-model.test.ts index c60af2a802..60a11f2cec 100644 --- a/apps/desktop/src/main/__tests__/workbar-model.test.ts +++ b/apps/desktop/src/main/__tests__/workbar-model.test.ts @@ -74,15 +74,15 @@ describe('Workbar topology', () => { state = reduceWorkbarPanels(state, { type: 'open', placement: 'right', - tab: { id: 'workbar:tasks', kind: 'tasks' }, + tab: { id: 'workbar:work-board', kind: 'work-board' }, }); state = reduceWorkbarPanels(state, { type: 'move-to-panel', - tabId: 'workbar:tasks', + tabId: 'workbar:work-board', target: 'bottom', }); assert.deepEqual(state.right.tabs.map((tab) => tab.id), ['workbar:review']); - assert.deepEqual(state.bottom.tabs.map((tab) => tab.id), ['workbar:tasks']); + assert.deepEqual(state.bottom.tabs.map((tab) => tab.id), ['workbar:work-board']); assert.equal(state.focusedPanel, 'bottom'); }); @@ -109,7 +109,7 @@ describe('Workbar topology', () => { state = reduceWorkbarLayout(state, { type: 'open', placement: 'bottom', - tab: { id: 'workbar:tasks', kind: 'tasks' }, + tab: { id: 'workbar:work-board', kind: 'work-board' }, }); assert.equal(state.bottomOpen, true); state = reduceWorkbarLayout(state, { @@ -121,7 +121,7 @@ describe('Workbar topology', () => { state = reduceWorkbarLayout(state, { type: 'close', placement: 'bottom', - tabIds: ['workbar:tasks'], + tabIds: ['workbar:work-board'], }); assert.equal(state.bottomOpen, false); }); @@ -138,7 +138,6 @@ describe('Workbar topology', () => { ownerSessionId: 'session-a', }, { id: 'side-chat:panel-a', kind: 'side-chat', ordinal: 2 }, - { id: 'workbar:files', kind: 'files', preview: true }, ]), ); assert.deepEqual(persistableSessionWorkbarPanels(state).right.tabs, [ @@ -183,6 +182,36 @@ describe('Workbar topology', () => { assert.deepEqual(readSessionWorkbarPanels(), createSessionWorkbarPanelsState()); }); + it('drops a retired tool kind left in v3 storage', () => { + // An install that had the Task face open before it was retired still has + // `workbar:tasks` in v3 storage. The kind no longer exists, so the tab has + // no panel to render; it must be dropped rather than restored as a tab + // whose content is null, and the panel must open on what is left. + cleanups.push( + installMemoryLocalStorage({ + 'maka-session-workbar-panels-v3': JSON.stringify({ + version: 3, + right: { + version: 2, + tabs: [ + { id: 'workbar:tasks', kind: 'tasks' }, + { id: 'workbar:review', kind: 'review' }, + ], + activeTabId: 'workbar:tasks', + }, + bottom: { version: 2, tabs: [], activeTabId: null }, + focusedPanel: 'right', + }), + }), + ); + const state = readSessionWorkbarPanels(); + assert.deepEqual( + state.right.tabs.map((tab) => tab.id), + ['workbar:review'], + ); + assert.equal(state.right.activeTabId, 'workbar:review'); + }); + it('round-trips v3 layout while filtering transient tab data', () => { cleanups.push(installMemoryLocalStorage()); const layout = { @@ -196,7 +225,6 @@ describe('Workbar topology', () => { resourceRef: 'run:round-trip', ownerSessionId: 'session-a', }, - { id: 'workbar:files', kind: 'files', preview: true }, ], 'workbar:review', ), diff --git a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts index 1b141ffa3c..05ba07dce8 100644 --- a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts +++ b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts @@ -33,7 +33,6 @@ function createBridgeRecorder(): { 'sessions.subscribeEvents', 'shellRuns.subscribePtyData', 'shellRuns.subscribeResync', - 'todo.subscribeChanges', 'browser.setActiveSession', 'browser.setViewport', 'browser.onState', @@ -70,7 +69,6 @@ function createBridgeRecorder(): { gitReview: domain('gitReview'), sessions: domain('sessions'), shellRuns: domain('shellRuns'), - todo: domain('todo'), browser: domain('browser'), artifacts: domain('artifacts'), app: domain('app'), @@ -122,9 +120,6 @@ describe('createDesktopWorkbarServices', () => { services.terminal.subscribePtyData(eventHandler)(); services.terminal.subscribeResync(eventHandler)(); - await services.todo.read('s'); - services.todo.subscribeChanges(eventHandler)(); - services.browser.setActiveSession('s'); services.browser.setViewport({ sessionId: 's', rect: null }); await services.browser.navigate('s', 'https://example.com'); @@ -197,8 +192,6 @@ describe('createDesktopWorkbarServices', () => { 'shellRuns.write', 'shellRuns.subscribePtyData', 'shellRuns.subscribeResync', - 'todo.read', - 'todo.subscribeChanges', 'browser.setActiveSession', 'browser.setViewport', 'browser.navigate', diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts index 6c741c5248..5b64d73e1d 100644 --- a/apps/desktop/src/main/e2e-fixture.ts +++ b/apps/desktop/src/main/e2e-fixture.ts @@ -180,7 +180,10 @@ export function getE2eFixtureState(fixture: E2eFixture | null): E2eFixtureState case 'settings-models': return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'models' }; case 'turn-narrative': - return { ...state, activeSessionId: TURN_SESSION_ID, workbarCollapsed: false, workbarTab: 'tasks' }; + // Any open face will do — the scenario is about focus order through the + // transcript, and the workbar is here only so the panel is on screen. + // This was the Task face until it was retired. + return { ...state, activeSessionId: TURN_SESSION_ID, workbarCollapsed: false, workbarTab: 'review' }; case 'turn-narrative-browser': return { ...state, activeSessionId: TURN_SESSION_ID, workbarCollapsed: false, workbarTab: 'browser' }; case 'chat-prompt-rail': @@ -247,24 +250,6 @@ export async function seedE2eFixture(input: { if (scenario === 'agent-graph-layout') await seedAgentGraphLayout(input.workspaceRoot, now); - if (scenario === 'turn-narrative' || scenario === 'turn-narrative-browser') { - const owner = await tryAcquireInteractiveRootOwner(storageRoot); - if (!owner) throw new Error('Unable to acquire the E2E fixture SessionTodo root'); - try { - const todos = await openInteractiveSessionTodoStoreForWrite(owner.lease); - try { - await todos.replaceAll(TURN_SESSION_ID, [ - { content: '补齐桌面端无障碍覆盖', status: 'in_progress' }, - { content: '核对模型选择器的键盘路径', status: 'pending' }, - { content: '确认工具结果可以展开阅读', status: 'completed' }, - ]); - } finally { - todos.close(); - } - } finally { - await owner.close(); - } - } if (scenario === 'chat-prompt-rail') { await writeSession(input.workspaceRoot, promptRailSession(now), promptRailMessages(now)); diff --git a/apps/desktop/src/renderer/app-shell-e2e-fixture.ts b/apps/desktop/src/renderer/app-shell-e2e-fixture.ts index b37fc542e1..102b9e4ade 100644 --- a/apps/desktop/src/renderer/app-shell-e2e-fixture.ts +++ b/apps/desktop/src/renderer/app-shell-e2e-fixture.ts @@ -132,7 +132,6 @@ export function createAppShellE2eFixtureActions(options: { if ( state.workbarTab === 'review' || state.workbarTab === 'terminal' || - state.workbarTab === 'tasks' || state.workbarTab === 'browser' || state.workbarTab === 'files' || state.workbarTab === 'inspector' diff --git a/apps/desktop/src/renderer/features/workbar/README.md b/apps/desktop/src/renderer/features/workbar/README.md index e8c6754ca2..85c141f985 100644 --- a/apps/desktop/src/renderer/features/workbar/README.md +++ b/apps/desktop/src/renderer/features/workbar/README.md @@ -52,12 +52,18 @@ remounted when the active session changes. ## Lifecycle invariants -- Review, Tasks, Browser, Files and Inspector tabs are persisted globally. -- Terminal and Side Chat tabs, preview state and resource metadata are - transient. +- Review, Work Board, Browser, Files and Inspector tabs are persisted globally. +- Terminal and Side Chat tabs and their resource metadata are transient. - `WORKBAR_TOOL_DEFINITIONS` is the authority for persistence, singleton - behavior and default placement; storage and controller code consume it - rather than maintaining parallel kind lists. + behavior, default placement, icon and shortcut; storage, controller and UI + code consume it rather than maintaining parallel kind lists. +- A face is opened and closed only from the strip's `[+]` menu, which lists + every registered tool and marks the open ones. Tabs carry no close control: + `Tab` renders `endContent` inside its own ` - - } - variant="ghost" - size="sm" - className="maka-workbar-tab-close" - onClick={() => props.onClose(props.tab)} - /> - - - - ); -} - function WorkbarLauncher(props: { onOpen: (kind: SessionWorkbarTabKind) => void; sideChatAvailable: boolean; }) { const copy = getDesktopConversationCopy(useUiLocale()).workbar; - const actions: Array<{ - kind: SessionWorkbarTabKind; - label: string; - description: string; - icon: typeof Activity; - shortcut?: string; - disabled?: boolean; - }> = [ - { - kind: 'side-chat', - label: copy.sideChat, - description: copy.launcher.sideChat, - icon: MessageCircleQuestion, - shortcut: 'mod+alt+s', - disabled: !props.sideChatAvailable, - }, - { - kind: 'review', - label: copy.review, - description: copy.launcher.review, - icon: GitBranch, - shortcut: 'ctrl+shift+g', - }, - { - kind: 'terminal', - label: copy.terminal, - description: copy.launcher.terminal, - icon: Terminal, - shortcut: 'ctrl+`', - }, - { - kind: 'browser', - label: copy.browser, - description: copy.launcher.browser, - icon: Globe, - shortcut: 'mod+t', - }, - { - kind: 'files', - label: copy.files, - description: copy.launcher.files, - icon: FolderOpen, - shortcut: 'mod+p', - }, - { - kind: 'tasks', - label: copy.tasks, - description: copy.launcher.tasks, - icon: ListTodo, - }, - { - kind: 'work-board', - label: copy.workBoard, - description: copy.launcher.workBoard, - icon: Clipboard, - }, - { - kind: 'inspector', - label: copy.inspector, - description: copy.launcher.inspector, - icon: Activity, - }, - ]; + // The list is the tool registry, in registry order — icons and shortcuts + // included. This is the one place a face's shortcut is shown, so it is also + // where the shortcuts are learned. return (
@@ -628,19 +344,19 @@ function WorkbarLauncher(props: { density="compact" header={{copy.openTools}} > - {actions.map((action) => ( + {WORKBAR_TOOL_DEFINITIONS.map((definition) => ( } - label={action.label} - description={action.description} + key={definition.kind} + startContent={ + + } + label={faceLabel(definition.kind, copy)} + description={copy.launcher[launcherCopyKey(definition.kind)]} endContent={ - action.shortcut ? ( - - ) : undefined + definition.shortcut ? : undefined } - isDisabled={action.disabled} - onClick={() => props.onOpen(action.kind)} + isDisabled={definition.kind === 'side-chat' && !props.sideChatAvailable} + onClick={() => props.onOpen(definition.kind)} /> ))} @@ -649,6 +365,16 @@ function WorkbarLauncher(props: { ); } +function launcherCopyKey( + kind: SessionWorkbarTabKind, +): keyof WorkbarCopy['launcher'] { + return kind === 'side-chat' + ? 'sideChat' + : kind === 'work-board' + ? 'workBoard' + : kind; +} + export function WorkbarSurface(props: { sessionId: string; projectId?: string | null; @@ -664,18 +390,6 @@ export function WorkbarSurface(props: { placement: SessionWorkbarPlacement, tabs: readonly SessionWorkbarTab[], ) => void; - onReorderTab: ( - placement: SessionWorkbarPlacement, - tabId: string, - targetTabId: string, - ) => void; - onMoveTab: ( - placement: SessionWorkbarPlacement, - tabId: string, - direction: 'left' | 'right', - ) => void; - onMoveTabToPanel: (tabId: string, target: SessionWorkbarPlacement) => void; - onPinTab: (tabId: string) => void; onOpenLauncher: (placement: SessionWorkbarPlacement) => void; onRequestOpenTab: ( placement: SessionWorkbarPlacement, @@ -696,11 +410,6 @@ export function WorkbarSurface(props: { }) { const locale = useUiLocale(); const copy = getDesktopConversationCopy(locale).workbar; - const sessionTodo = useSessionTodo(props.sessionId, { - locale, - loadFailed: copy.todoLoadFailed, - }); - const taskCount = sessionTodoActiveCount(sessionTodo.items); const [artifactCount, setArtifactCount] = useState(0); const placements: SessionWorkbarPlacement[] = ['right', 'bottom']; const positionedTabs = placements.flatMap((placement) => @@ -738,21 +447,17 @@ export function WorkbarSurface(props: { tabs={panel.tabs} activeTabId={showingLauncher ? null : panel.activeTabId} activeSideChatPanelIds={props.activeSideChatPanelIds} - taskCount={taskCount} artifactCount={artifactCount} + sideChatAvailable={props.sourceSession !== undefined} onActivate={(tabId) => props.onActivateTab(placement, tabId)} - onClose={(tab) => props.onCloseTab(placement, tab)} - onCloseTabs={(tabs) => props.onCloseTabs(placement, tabs)} - onReorder={(tabId, targetTabId) => - props.onReorderTab(placement, tabId, targetTabId) - } - onMove={(tabId, direction) => - props.onMoveTab(placement, tabId, direction) + onOpenKind={(kind) => props.onRequestOpenTab(placement, kind)} + onCloseKind={(kind) => + props.onCloseTabs( + placement, + panel.tabs.filter((tab) => tab.kind === kind), + ) } - onMoveToPanel={props.onMoveTabToPanel} - onPin={props.onPinTab} placement={placement} - onOpenLauncher={() => props.onOpenLauncher(placement)} onCollapseRightPanel={ placement === 'right' ? () => props.onDismissPanel('right') @@ -798,15 +503,6 @@ export function WorkbarSurface(props: { /> ); - } else if (tab.kind === 'tasks') { - content = ( - - ); } else if (tab.kind === 'work-board') { content = ( props.onPinTab(tab.id)} className={ tab.kind === 'side-chat' ? 'maka-quote-workbar-panel' : undefined } diff --git a/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx b/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx index 9ca749119f..55e858bf9a 100644 --- a/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx +++ b/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx @@ -23,9 +23,17 @@ import { IconButton, useUiLocale } from '@maka/ui'; import { PanelRightClose, PanelRightOpen } from '@maka/ui/icons'; import { getShellCopy } from '../../../locales/shell-copy'; -/** Shared titlebar/panel toggle for the Workbar column. */ +/** + * Shared titlebar/panel toggle for the Workbar column. + * + * `md` is the titlebar rail's size, shared with the sidebar and search + * actions it stands beside. In the workbar's own bar it stands beside the + * strip's `sm` tabs and the `sm` `[+]` instead, so that caller passes `sm` — + * three controls in one row have to report one height. + */ export function WorkbarToggle(props: { collapsed: boolean; + size?: 'sm' | 'md'; className?: string; onToggle(): void; }) { @@ -43,7 +51,7 @@ export function WorkbarToggle(props: { /> )} variant="ghost" - size="md" + size={props.size ?? 'md'} className={ props.className ? `maka-titlebar-action ${props.className}` @@ -71,7 +79,9 @@ export function WorkbarTitlebarActions(props: { role="toolbar" aria-label={copy.workspaceActions} > - + {/* `sm`, like the toggle in the workbar's own bar: this is that control, + standing where it stood, so collapsing must not resize it. */} +
); } diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index b4fef1cfa2..4b6280b0bd 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -96,8 +96,6 @@ export interface DesktopConversationCopy { review: string; terminal: string; terminalNumbered(index: number): string; - tasks: string; - todoLoadFailed: string; workBoard: string; browser: string; files: string; @@ -106,21 +104,9 @@ export interface DesktopConversationCopy { sideChatNumbered(index: number): string; openTab: string; openTools: string; - closeTab(label: string): string; - tabMenu(label: string): string; - moveLeft: string; - moveRight: string; - moveToRight: string; - moveToBottom: string; - pinTab: string; - pinTabHint: string; - close: string; - closeOthers: string; - closeToRight: string; launcher: { review: string; terminal: string; - tasks: string; workBoard: string; browser: string; files: string; @@ -486,31 +472,17 @@ const COPY = { review: '变更', terminal: '终端', terminalNumbered: (index) => `终端 ${index}`, - tasks: '待办', - todoLoadFailed: '待办载入失败,请重试。', workBoard: '工作看板', browser: '浏览器', files: '生成文件', inspector: '追踪', sideChat: '侧边对话', sideChatNumbered: (index) => `侧边对话 ${index}`, - openTab: '打开工作栏标签', + openTab: '打开或关闭工作栏的面', openTools: '打开工具', - closeTab: (label) => `关闭${label}`, - tabMenu: (label) => `${label}标签菜单`, - moveLeft: '向左移动', - moveRight: '向右移动', - moveToRight: '移动到右侧面板', - moveToBottom: '移动到底部面板', - pinTab: '固定标签', - pinTabHint: '预览标签,双击或在内容中操作即可固定', - close: '关闭', - closeOthers: '关闭其他标签', - closeToRight: '关闭右侧标签', launcher: { review: '查看当前 Git 工作区变化', terminal: '查看当前任务的终端运行和实时输出', - tasks: '查看和维护这个任务的待办台账', workBoard: '记录和管理暂缓事项', browser: '打开内置浏览器并保留当前页面', files: '浏览当前任务生成的文件', @@ -740,31 +712,17 @@ const COPY = { review: '變更', terminal: '終端', terminalNumbered: (index) => `終端 ${index}`, - tasks: '待辦', - todoLoadFailed: '待辦載入失敗,請重試。', workBoard: '工作看板', browser: '瀏覽器', files: '生成檔案', inspector: '追蹤', sideChat: '側邊對話', sideChatNumbered: (index) => `側邊對話 ${index}`, - openTab: '開啟工作欄標籤', + openTab: '開啟或關閉工作欄的面', openTools: '開啟工具', - closeTab: (label) => `關閉${label}`, - tabMenu: (label) => `${label}標籤選單`, - moveLeft: '向左移動', - moveRight: '向右移動', - moveToRight: '移動到右側面板', - moveToBottom: '移動到底部面板', - pinTab: '固定標籤', - pinTabHint: '預覽標籤,雙擊或在內容中操作即可固定', - close: '關閉', - closeOthers: '關閉其他標籤', - closeToRight: '關閉右側標籤', launcher: { review: '檢視目前 Git 工作區變化', terminal: '檢視目前任務的終端執行和即時輸出', - tasks: '檢視和維護這個任務的待辦臺賬', workBoard: '記錄和管理暫緩事項', browser: '開啟內建瀏覽器並保留目前頁面', files: '瀏覽目前任務生成的檔案', @@ -985,31 +943,17 @@ const COPY = { review: 'Changes', terminal: 'Terminal', terminalNumbered: (index) => `Terminal ${index}`, - tasks: 'To-do', - todoLoadFailed: 'Failed to load the to-do list. Try again.', workBoard: 'Work board', browser: 'Browser', files: 'Generated files', inspector: 'Trace', sideChat: 'Side chat', sideChatNumbered: (index) => `Side chat ${index}`, - openTab: 'Open workbar tab', + openTab: 'Open or close a workbar face', openTools: 'Open tools', - closeTab: (label) => `Close ${label}`, - tabMenu: (label) => `${label} tab menu`, - moveLeft: 'Move left', - moveRight: 'Move right', - moveToRight: 'Move to right panel', - moveToBottom: 'Move to bottom panel', - pinTab: 'Pin tab', - pinTabHint: 'Preview tab. Double-click or interact with its content to pin it', - close: 'Close', - closeOthers: 'Close other tabs', - closeToRight: 'Close tabs to the right', launcher: { review: 'View changes in the current Git workspace', terminal: 'Inspect terminal runs and live output for this task', - tasks: "View and maintain this task's to-do ledger", workBoard: 'Capture and manage deferred work', browser: 'Open the embedded browser and keep the current page', files: 'Browse files generated by this task', diff --git a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts index 9e6ddbf21f..8313458a27 100644 --- a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts +++ b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts @@ -31,7 +31,6 @@ export type DesktopWorkbarBridge = Pick< | 'inspector' | 'sessions' | 'shellRuns' - | 'todo' | 'transcripts' >; @@ -55,7 +54,6 @@ export function createDesktopWorkbarServices( bridge.sessions.subscribeEvents(sessionId, handler), }, terminal: bridge.shellRuns, - todo: bridge.todo, browser: { setActiveSession: (sessionId) => bridge.browser.setActiveSession(sessionId), setViewport: (input) => bridge.browser.setViewport(input), diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 02e5062d64..d07f44133b 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -47,7 +47,6 @@ @import "./styles/settings/select.css" layer(components); @import "./styles/model-switcher.css" layer(components); @import "./styles/chat-header.css" layer(components); -@import "./styles/session-todo-panel.css" layer(components); @import "./styles/work-board.css" layer(components); @import "./styles/plan-mode.css" layer(components); @import "./styles/agent-graph.css" layer(components); diff --git a/apps/desktop/src/renderer/styles/session-todo-panel.css b/apps/desktop/src/renderer/styles/session-todo-panel.css deleted file mode 100644 index f4df8be5ac..0000000000 --- a/apps/desktop/src/renderer/styles/session-todo-panel.css +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* SessionTodoPanel (#4351) — the read-only, flat projection of the Host-owned - current Todo document. It replaced the nested Task Ledger demand chain, so - this sheet was reduced (#4394) to what the flat list actually renders: a - `-panel` scroll container, a `-tree`
    , and one `-row`
  1. per item that - holds a status icon and its content text. The retired four-column, - depth-indented tree row — its `--task-depth` indent, `-group`, - `[data-status]`, `-key`/`-subject`/`-meta`/`-detail`, and `-terminal` - descendants — is gone; none of those elements exist anymore. */ - -.maka-session-todo-panel { - height: 100%; - min-height: 0; - overflow-y: auto; - padding: var(--space-2); -} - -.maka-session-todo-tree { - display: grid; - gap: var(--border-width-hairline); -} - -/* Two children only: the status icon, then content that takes the remaining - width and wraps within it (`minmax(0, 1fr)` lets the text column shrink so - long, unbroken content wraps instead of overflowing the workbar). The - workbar resizes down to SESSION_WORKBAR_MIN_WIDTH (320px) inside an - otherwise wide window, so wrapping — not a width breakpoint — is what keeps - the row readable at every width. If width-responsive styling is ever added - here it should be an `@container` query on `.maka-session-todo-panel` (see - workbar/inspector.css and chat-header.css), not a viewport `@media`. */ -.maka-session-todo-row { - font: var(--maka-text-supporting); - display: grid; - grid-template-columns: auto minmax(0, 1fr); - align-items: center; - gap: var(--space-2); - min-height: 32px; - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-control); - color: var(--muted-foreground); -} - -.maka-session-todo-row > span { - min-width: 0; - overflow-wrap: anywhere; -} - -.maka-session-todo-row:hover { background: var(--state-hover-bg); } - -.maka-session-todo-message { - font: var(--maka-text-supporting); - display: flex; - align-items: center; - justify-content: space-between; - min-height: 32px; - padding: var(--space-1) var(--space-2); - color: var(--muted-foreground); -} - -.maka-session-todo-retry { - display: inline-grid; - width: 28px; - height: 28px; - place-items: center; - border: 0; - border-radius: var(--radius-control); - background: transparent; - color: inherit; -} - -.maka-session-todo-retry:hover { background: var(--state-hover-bg); } - -.maka-session-todo-retry:focus-visible { - outline: var(--focus-ring-width) solid var(--focus-ring); - outline-offset: 1px; -} diff --git a/apps/desktop/src/renderer/styles/workbar/shell.css b/apps/desktop/src/renderer/styles/workbar/shell.css index 444ad1567d..f62b09e407 100644 --- a/apps/desktop/src/renderer/styles/workbar/shell.css +++ b/apps/desktop/src/renderer/styles/workbar/shell.css @@ -128,6 +128,33 @@ align-items: center; min-width: 0; box-sizing: border-box; + /* The rail belongs to the bar, not to the tab strip. `TabList hasDivider` + draws it on the strip's own box, which ends where the tabs do, so it ran + out from under `[+]` and the collapse toggle and read as a line beneath + half a row. This is the second idiom Astryx's `Tab` names for the rail + ("TabList `hasDivider` or a Toolbar with a bottom divider"): the bar + carries it, and hands the tabs `--_tab-indicator-bottom` so the selected + indicator drops through the bar's own gap and lands on it. + + It is an `::after` rather than a border because the bar's height is the + titlebar clearance it stands in; a border would take a pixel out of that + and leave the 28px controls centred on a half pixel. Each placement + declares `--maka-workbar-rail-gap`: the distance from a control's bottom + edge down to the rail, which is what the indicator has to travel. Astryx's + own Toolbar adds a border width on top of that, because its rail is a + border and so sits outside the box the padding measures to; ours is inside + it, so the gap alone lands the indicator's bottom edge on the rail. */ + --_tab-indicator-bottom: calc(-1 * var(--maka-workbar-rail-gap)); +} + +.maka-session-workbar-toolbar::after { + content: ""; + position: absolute; + inset-inline: 0; + inset-block-end: 0; + block-size: var(--border-width); + background: var(--color-border); + pointer-events: none; } .maka-session-workbar[data-placement="right"] > .maka-session-workbar-toolbar { @@ -136,6 +163,17 @@ above that hit-test surface while its empty space remains draggable. */ z-index: calc(var(--z-titlebar) + 1); height: var(--maka-plate-titlebar-clearance); + /* The controls are centred in the bar, so half of what the clearance leaves + over an `sm` control sits under them. */ + --maka-workbar-rail-gap: calc( + (var(--maka-plate-titlebar-clearance) - var(--size-element-sm)) / 2 + ); + /* The right pad is the window titlebar strip's own gutter, not this bar's: + the collapse toggle is one control that moves between the two bands, and + `session-workbar.spec.ts` holds it to the same x in both. Narrowing this to + the bar's own gutter would slide it 16px on every collapse. On top of it, + the caption buttons where the platform draws them on the right (Windows); + macOS puts them on the left, over the sidebar, and reports 0 here. */ padding-inline: var(--space-2) calc(var(--space-6) + var(--maka-titlebar-overlay-right-width)); -webkit-app-region: drag; @@ -149,6 +187,8 @@ .maka-session-workbar[data-placement="bottom"] > .maka-session-workbar-toolbar { padding: var(--space-2) var(--space-4); + /* Height is content here, so the gap under a control is the block padding. */ + --maka-workbar-rail-gap: var(--space-2); } .maka-session-workbar > .maka-lazy-fallback { @@ -158,131 +198,28 @@ .maka-workbar-tab-strip { display: flex; align-items: center; + gap: var(--space-1); width: 100%; min-width: 0; flex: 0 0 auto; + /* The bar's height comes from the titlebar clearance it stands in, never from + the strip: letting the strip set it would move the whole panel the moment + its content changed height. */ + height: 100%; } +/* A flex item defaults to `min-width: auto`, so without this reset the strip + refuses to shrink: it spills past the panel and pushes [+] and the collapse + toggle off the edge instead of scrolling inside itself. */ .maka-workbar-tab-list { - display: flex; - align-items: center; flex: 1 1 auto; - gap: var(--space-1); min-width: 0; - overflow-x: auto; - overscroll-behavior-x: contain; - scroll-padding-inline: var(--space-1); - scrollbar-width: none; -} - -.maka-workbar-tab-list::-webkit-scrollbar { - display: none; -} - -.maka-workbar-tab-context { - flex: 0 0 auto; - min-width: 0; -} - -.maka-workbar-tab { - position: relative; - display: inline-flex; - align-items: center; - flex: 0 0 auto; - min-width: 0; - height: var(--size-element-sm); - border-radius: var(--radius-element); - color: var(--muted-foreground); - touch-action: none; - user-select: none; - transition: - background var(--duration-quick) var(--ease-out-strong), - color var(--duration-quick) var(--ease-out-strong); -} - -.maka-workbar-tab:hover, -.maka-workbar-tab[data-active] { - background: var(--state-hover-bg); - color: var(--foreground); -} - -.maka-workbar-tab[data-active]::after { - position: absolute; - right: var(--space-2); - bottom: -1px; - left: var(--space-2); - height: 2px; - border-radius: var(--radius-full); - background: var(--color-accent); - content: ""; -} - -.maka-workbar-tab[data-dragging] { - z-index: 2; - opacity: 0.72; - box-shadow: var(--shadow-low); -} - -.maka-workbar-tab-icon { - width: var(--icon-control); - height: var(--icon-control); - flex: 0 0 auto; } .maka-workbar-tab-spinner { animation: maka-spin 1s linear infinite; } -.maka-workbar-tab-select { - display: inline-flex; - align-items: center; - gap: var(--space-1); - min-width: 0; - min-height: 0; - height: 100%; - padding: 0 var(--space-1) 0 var(--space-3); - border: 0; - border-radius: 0; - background: transparent; - color: inherit; - cursor: pointer; - font: var(--maka-text-supporting); - box-shadow: none; -} - -.maka-workbar-tab[data-active] .maka-workbar-tab-select { - font-weight: var(--font-weight-semibold); -} - -.maka-workbar-tab-select:focus-visible { - border-radius: var(--radius-element); - outline: var(--focus-ring-width) solid var(--focus-ring); - outline-offset: calc(-1 * var(--focus-ring-width)); -} - -.maka-workbar-tab[data-preview] .maka-workbar-tab-label { - font-style: italic; -} - -.maka-workbar-tab-label { - overflow: hidden; - max-width: 112px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.maka-workbar-tab-close { - position: relative; - z-index: 1; - flex: 0 0 auto; - margin-right: 1px; -} - -.maka-workbar-new-tab { - flex: 0 0 auto; - margin-left: var(--space-1); -} - .maka-workbar-panel-toggle { flex: 0 0 auto; margin-left: var(--space-1); diff --git a/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx b/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx index 3bd8066b29..ffc98ef4ed 100644 --- a/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx +++ b/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx @@ -95,10 +95,6 @@ function WorkbarToolSurface(props: { kind: 'terminal' | 'browser' | 'files' }) { onActivateTab={noop} onCloseTab={noop} onCloseTabs={noop} - onReorderTab={noop} - onMoveTab={noop} - onMoveTabToPanel={noop} - onPinTab={noop} onOpenLauncher={noop} onRequestOpenTab={noop} confirmBypass={async () => true} diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index 71329da3c1..f12f2f7989 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -24,7 +24,6 @@ import type { ArtifactRecord } from '@maka/core/artifacts'; import type { BrowserState } from '@maka/core/browser'; import type { GitReviewReadResult, GitReviewSnapshot } from '@maka/core/git-review'; import type { SessionSummary } from '@maka/core/session'; -import type { SessionTodoItem } from '@maka/core/session-todo'; import type { SessionTrace } from '@maka/core/session-trace'; import type { ContextDiagnosticsResult } from '@maka/runtime-host/protocol'; import { ToastProvider } from '@maka/ui'; @@ -33,6 +32,7 @@ import { WorkbarSurface } from '../src/renderer/features/workbar/stories'; import { createFakeWorkbarServices, createSessionWorkbarPanelsState, + activateSessionWorkbarTab, createSessionWorkbarTabsState, openStaticSessionWorkbarTab, terminalSessionWorkbarTabId, @@ -147,21 +147,6 @@ const RICH_TERMINAL_BUFFER = [ // ---- ledgers ------------------------------------------------------------- -// The long item is deliberate: it is what proves a long subject still wraps -// instead of pushing the panel sideways. -const tasks: SessionTodoItem[] = [ - { content: '完成会话任务台账升级', status: 'in_progress' }, - { content: '验证 SQLite authority 与并发短 key 分配', status: 'completed' }, - { content: '检查窄窗口下的任务树布局', status: 'pending' }, - { - content: - '核对深层缩进、超长任务描述、owner 与阻塞原因在窄窗口中仍可完整换行且不遮挡后续内容', - status: 'pending', - }, - { content: '同步生命周期文档与边界说明', status: 'pending' }, - { content: '验证 Goal 一次提醒门禁', status: 'completed' }, -]; - const artifacts: ArtifactRecord[] = [ { id: 'artifact-patch', @@ -776,8 +761,6 @@ const unsubscribe = () => () => undefined; * varies, and everything else stays on the populated default. */ function bridge(options: { - tasks?: SessionTodoItem[]; - tasksFail?: boolean; trace?: SessionTrace; traceNextCursor?: string; traceFail?: boolean; @@ -799,13 +782,6 @@ function bridge(options: { } = {}): Decorator { const browserState = options.browserState ?? EMPTY_BROWSER_STATE; const services = createFakeWorkbarServices({ - todo: { - read: async () => { - if (options.tasksFail) throw new Error('读取任务失败'); - return options.tasks ?? tasks; - }, - subscribeChanges: unsubscribe, - }, artifacts: { list: async () => artifacts, readText: async (_sessionId: string, id: string) => ({ ok: true, text: artifactText[id] ?? '' }), @@ -950,6 +926,8 @@ function bridge(options: { */ function Workbar(props: { tab?: SessionWorkbarTabKind; + /** Extra faces opened after `tab`, so the strip can be seen with several. */ + alsoOpen?: readonly Exclude[]; sourceSession?: SessionSummary; /** Overrides the restored column width, the way the resize handle does. */ width?: number; @@ -979,11 +957,22 @@ function Workbar(props: { }, ]; } - const tabsState = tab + const openedFirst = tab ? createSessionWorkbarTabsState([tab], tab.id) : props.tab && props.tab !== 'side-chat' ? openStaticSessionWorkbarTab(emptyTabsState, props.tab) : emptyTabsState; + // Opening a face activates it, so after the extras land the requested face + // is re-activated: the strip shows several tabs with `props.tab` selected + // and the rest unselected, which is the only arrangement where a hovered + // unselected tab can be told apart from the selected one. + const withExtras = (props.alsoOpen ?? []).reduce( + (state, kind) => openStaticSessionWorkbarTab(state, kind), + openedFirst, + ); + const tabsState = openedFirst.activeTabId + ? activateSessionWorkbarTab(withExtras, openedFirst.activeTabId) + : withExtras; return (
    @@ -1006,10 +1000,6 @@ function Workbar(props: { onActivateTab={noop} onCloseTab={noop} onCloseTabs={noop} - onReorderTab={noop} - onMoveTab={noop} - onMoveTabToPanel={noop} - onPinTab={noop} onOpenLauncher={noop} onRequestOpenTab={noop} confirmBypass={async () => true} @@ -1048,6 +1038,26 @@ export const Changes: Story = { render: () => , }; +// Real path: 变更 open, then 浏览器 and 生成文件 opened from [+]. Faces are added to +// the right of the strip and never reordered, so this is what three of them +// look like — one selected, two not, which is the only arrangement where the +// selected marker can be told apart from a hover. (Work Board is not among +// them: this story group's bridge stubs no `workBoard` service, and the panel +// subscribes to it on mount.) +export const SeveralFaces: Story = { + decorators: [bridge()], + render: () => , +}; + +// The same three at the panel's floor, where the strip has to scroll inside +// itself rather than push [+] and the collapse toggle off the edge. +export const SeveralFacesAtColumnFloor: Story = { + decorators: [bridge()], + render: () => ( + + ), +}; + // Real path: 任务工作栏 → 变更 on a session whose branch matches its base. The // panel's own empty state (icon + help), not a spinner and not an error. export const ChangesEmpty: Story = { @@ -1190,26 +1200,6 @@ export const TerminalWriteFailed: Story = { }, }; -// Real path: sidebar → a session → 展开任务工作栏, landing on the tab the app -// restored. Tasks is the default: an in-progress root, a child claimed and -// blocked by a subagent, and the finished ones folded into 最近结束. -export const Tasks: Story = { - decorators: [bridge()], - render: () => , -}; - -// Real path: 任务工作栏 → 任务 on a session whose agent never wrote a task. -export const TasksEmpty: Story = { - decorators: [bridge({ tasks: [] })], - render: () => , -}; - -// Real path: 任务工作栏 → 任务 when `tasks.list` rejects; 重试 re-runs the read. -export const TasksLoadFailed: Story = { - decorators: [bridge({ tasksFail: true })], - render: () => , -}; - // Storybook cannot host the native WebContentsView, so these pin what the panel // itself draws — chrome and empty state — inside the real workbar shell. export const BrowserEmpty: Story = { diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 6a8def3d1f..274904ffde 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.5.2` (194 component exports). Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 249 files — blocker 0, reimplementation 0, polish 1, aligned 248. +**Totals:** 247 files — blocker 0, reimplementation 0, polish 1, aligned 246. ## Exclusions (explicit) @@ -88,7 +88,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState | aligned — uses Astryx (Banner, EmptyState) | aligned | | `apps/desktop/src/renderer/features/workbar/ui/side-chat-close-confirmation.tsx` | shell-chrome-or-panel | Button, CheckboxInput, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, Text, VStack | aligned — uses Astryx (Button, CheckboxInput, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter) | aligned | | `apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx` | shell-chrome-or-panel | Card, ResizeHandle, Spinner | aligned — uses Astryx (Card, ResizeHandle, Spinner) | aligned | -| `apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx` | shell-chrome-or-panel | Badge, Button, Card, ContextMenu, Heading, Icon, IconButton, Kbd, List, ListItem, Section, Spinner, Tooltip | aligned — uses Astryx (Badge, Button, Card, ContextMenu, Heading, Icon, IconButton, Kbd) | aligned | +| `apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx` | shell-chrome-or-panel | Badge, Card, DropdownMenu, DropdownMenuItem, Heading, Icon, Kbd, List, ListItem, Section, Spinner, Tab, TabList | aligned — uses Astryx (Badge, Card, DropdownMenu, DropdownMenuItem, Heading, Icon, Kbd, List) | aligned | | `apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx` | shell-chrome-or-panel | Icon, IconButton, Tooltip | aligned — uses Astryx (Icon, IconButton, Tooltip) | aligned | | `apps/desktop/src/renderer/keyboard-help.tsx` | dialog-overlay | Dialog, DialogHeader, Heading, Kbd, Layout, LayoutContent | aligned — uses Astryx (Dialog, DialogHeader, Heading, Kbd, Layout, LayoutContent) | aligned | | `apps/desktop/src/renderer/live-turn-reconciler.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | @@ -180,7 +180,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/styles/prompt-suggestions.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/quote-side-panel.css` | shell-chrome-or-panel | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/search-modal.css` | dialog-overlay | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | -| `apps/desktop/src/renderer/styles/session-todo-panel.css` | shell-chrome-or-panel | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/bot.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/connection.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | @@ -262,7 +261,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `packages/ui/src/session-rail-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/session-rename-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput) | aligned | | `packages/ui/src/session-sidebar-nav.tsx` | shell-chrome-or-panel | Icon, IconButton, SideNavItem, SideNavSection, Tooltip | aligned — uses Astryx (Icon, IconButton, SideNavItem, SideNavSection, Tooltip) | aligned | -| `packages/ui/src/session-todo-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState, IconButton, Spinner | aligned — uses Astryx (Banner, EmptyState, IconButton, Spinner) | aligned | | `packages/ui/src/skill-inspector.tsx` | shell-chrome-or-panel | Button, Divider, HStack, Heading, MetadataList, MetadataListItem, StackItem, StatusDot, Switch, Text, VStack | aligned — uses Astryx (Button, Divider, HStack, Heading, MetadataList, MetadataListItem, StackItem, StatusDot) | aligned | | `packages/ui/src/skills-panel.tsx` | module-hub | Button, DropdownMenu, DropdownMenuItem, EmptyState, IconButton, List, ListItem, SegmentedControl, SegmentedControlItem, Selector, StatusDot, Text, TextInput, Toolbar | aligned — uses Astryx (Button, DropdownMenu, DropdownMenuItem, EmptyState, IconButton, List, ListItem, SegmentedControl) | aligned | | `packages/ui/src/styles.css` | ui-composition | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index a00a5e0e85..557f6c2ef9 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -151,7 +151,6 @@ apps/desktop/src/renderer/styles/prompt-rail.css apps/desktop/src/renderer/styles/prompt-suggestions.css apps/desktop/src/renderer/styles/quote-side-panel.css apps/desktop/src/renderer/styles/search-modal.css -apps/desktop/src/renderer/styles/session-todo-panel.css apps/desktop/src/renderer/styles/settings.css apps/desktop/src/renderer/styles/settings/bot.css apps/desktop/src/renderer/styles/settings/connection.css @@ -233,7 +232,6 @@ packages/ui/src/session-list-panel.tsx packages/ui/src/session-rail-context.tsx packages/ui/src/session-rename-dialog.tsx packages/ui/src/session-sidebar-nav.tsx -packages/ui/src/session-todo-panel.tsx packages/ui/src/skill-inspector.tsx packages/ui/src/skills-panel.tsx packages/ui/src/styles.css diff --git a/package-lock.json b/package-lock.json index 321c4a5390..872cfd700d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,8 +66,6 @@ "@astryxdesign/core": "0.5.2", "@astryxdesign/theme-neutral": "0.5.2", "@babel/parser": "7.29.7", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", "@maka/ui": "0.1.0", @@ -1667,63 +1665,6 @@ "node": ">=0.1.90" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@earendil-works/pi-tui": { "version": "0.84.4", "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.4.tgz", diff --git a/packages/ui/src/__tests__/session-todo-panel.test.tsx b/packages/ui/src/__tests__/session-todo-panel.test.tsx deleted file mode 100644 index 94ff4f71b2..0000000000 --- a/packages/ui/src/__tests__/session-todo-panel.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { LocaleProvider } from '../locale-context.js'; -import { SessionTodoPanel, sessionTodoActiveCount } from '../session-todo-panel.js'; - -test('renders the Host snapshot as one flat ordered list', () => { - const items = [ - { content: 'First pending item', status: 'pending' as const }, - { content: 'Second completed item', status: 'completed' as const }, - { content: 'Third active item', status: 'in_progress' as const }, - ]; - assert.equal(sessionTodoActiveCount(items), 2); - - const markup = renderToStaticMarkup( - - - , - ); - assert.ok(markup.indexOf('First pending item') < markup.indexOf('Second completed item')); - assert.ok(markup.indexOf('Second completed item') < markup.indexOf('Third active item')); - assert.equal(markup.includes('Task Create'), false); - assert.equal(markup.includes('T1'), false); -}); diff --git a/packages/ui/src/icons.tsx b/packages/ui/src/icons.tsx index 7c9e29a036..31c4b9e326 100644 --- a/packages/ui/src/icons.tsx +++ b/packages/ui/src/icons.tsx @@ -88,6 +88,7 @@ export { Eye, EyeOff, FileCode, + FileDiff, FileEdit, FileImage, FileText, diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 9a31cf091e..267a4c1fff 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -62,7 +62,6 @@ export * from './form-interaction-prompt.js'; export * from './form-interaction-prompt-state.js'; export * from './redact.js'; export * from './thinking-stream.js'; -export * from './session-todo-panel.js'; export * from './toast.js'; export * from './tool-output-stream.js'; export * from './ui.js'; diff --git a/packages/ui/src/session-todo-panel.tsx b/packages/ui/src/session-todo-panel.tsx deleted file mode 100644 index 35989a4116..0000000000 --- a/packages/ui/src/session-todo-panel.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Banner, EmptyState, IconButton, Spinner } from '@astryxdesign/core'; -import type { SessionTodoItem, SessionTodoStatus } from '@maka/core/session-todo'; -import { CheckCircle2, CircleGauge, Clock, ICON_SIZE, ListTodo, RefreshCcw } from './icons.js'; -import { useUiLocale } from './locale-context.js'; -import { getSharedUiCopy } from './shared-ui-copy.js'; - -const STATUS_ICONS = { - pending: Clock, - in_progress: CircleGauge, - completed: CheckCircle2, -} satisfies Record; - -export interface SessionTodoPanelProps { - items: readonly SessionTodoItem[]; - loading?: boolean; - error?: string; - onRetry?: () => void; -} - -export function sessionTodoActiveCount(items: readonly SessionTodoItem[]): number { - return items.filter((item) => item.status !== 'completed').length; -} - -/** Read-only flat projection of the Host-owned current Todo document. */ -export function SessionTodoPanel(props: SessionTodoPanelProps) { - const copy = getSharedUiCopy(useUiLocale()).sessionTodo; - return ( -
    - {props.error ? ( -
    - ); -} diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 098528ce3e..9784398d2f 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -95,13 +95,6 @@ export interface SharedUiCopy { close: string; resizeHandle: string; }; - sessionTodo: { - ariaLabel: string; - retry: string; - loading: string; - activeAriaLabel: string; - empty: string; - }; toast: { notifications: string; closeNotification: string; @@ -192,13 +185,6 @@ const SHARED_UI_COPY = { dailyReviewDisconnectedBody: '桌面端数据桥当前未连接。', }, primitives: { loading: '加载中', close: '关闭', resizeHandle: '调整宽度' }, - sessionTodo: { - ariaLabel: '任务待办', - retry: '重新载入待办', - loading: '正在载入待办…', - activeAriaLabel: '进行中的待办', - empty: '这个任务还没有待办', - }, toast: { notifications: '通知', closeNotification: '关闭通知', confirm: '确定', cancel: '取消' }, stream: { assistantChunkTruncated: '\n[…单条 delta 已截断]\n', assistantTailTruncated: '\n\n[…后续已截断]', thinkingHeadTruncated: '[…已截断早期 reasoning]\n', thinkingChunkTruncated: '\n[…单条 delta 已截断]\n', toolChunkTruncated: '\n[…已截断]\n' }, artifact: { unknownSize: '未知大小' }, @@ -276,13 +262,6 @@ const SHARED_UI_COPY = { dailyReviewDisconnectedBody: '桌面端資料橋目前未連線。', }, primitives: { loading: '載入中', close: '關閉', resizeHandle: '調整寬度' }, - sessionTodo: { - ariaLabel: '任務待辦', - retry: '重新載入待辦', - loading: '正在載入待辦…', - activeAriaLabel: '進行中的待辦', - empty: '這個任務還沒有待辦', - }, toast: { notifications: '通知', closeNotification: '關閉通知', confirm: '確定', cancel: '取消' }, stream: { assistantChunkTruncated: '\n[…單條 delta 已截斷]\n', assistantTailTruncated: '\n\n[…後續已截斷]', thinkingHeadTruncated: '[…已截斷早期 reasoning]\n', thinkingChunkTruncated: '\n[…單條 delta 已截斷]\n', toolChunkTruncated: '\n[…已截斷]\n' }, artifact: { unknownSize: '未知大小' }, @@ -360,13 +339,6 @@ const SHARED_UI_COPY = { dailyReviewDisconnectedBody: 'The desktop data bridge is not connected.', }, primitives: { loading: 'Loading', close: 'Close', resizeHandle: 'Resize handle' }, - sessionTodo: { - ariaLabel: 'To-do list', - retry: 'Reload the to-do list', - loading: 'Loading the to-do list…', - activeAriaLabel: 'In-progress to-dos', - empty: 'This task has no to-dos yet', - }, toast: { notifications: 'Notifications', closeNotification: 'Close notification', confirm: 'Confirm', cancel: 'Cancel' }, stream: { assistantChunkTruncated: '\n[…single delta truncated]\n', assistantTailTruncated: '\n\n[…remaining output truncated]', thinkingHeadTruncated: '[…earlier reasoning truncated]\n', thinkingChunkTruncated: '\n[…single delta truncated]\n', toolChunkTruncated: '\n[…truncated]\n' }, artifact: { unknownSize: 'Unknown size' },