From 0ed1be17733b1f7cb6365c5624526dd417dcef63 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Thu, 24 Sep 2026 00:34:08 +0800 Subject: [PATCH] fix: widen project picker beyond the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Float the project picker at 380px with viewport bounds and preserve menu focus and dismissal. Align local package metadata and packaged release notes with Desktop 0.13.13. Release note: 项目下拉列表可浮出侧栏,宽度调整为 380px,窄窗口自动收窄。 --- UI-DESIGN.md | 1 + package-lock.json | 4 +-- package.json | 2 +- resources/release-notes.json | 28 +++++++++++++++++++ src/renderer/src/ProjectSwitcher.test.tsx | 27 +++++++++++++++++++ src/renderer/src/ProjectSwitcher.tsx | 33 +++++++++++++++++++++-- src/renderer/src/styles.css | 13 ++++----- 7 files changed, 97 insertions(+), 11 deletions(-) diff --git a/UI-DESIGN.md b/UI-DESIGN.md index b0312684..29b860a6 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -290,6 +290,7 @@ - 固定导航行高至少 `36px`、行间距 `4px`,图标统一为 `16px`;导航与最近会话分隔线上下各留 `16px`。普通待办和待处理建议数量使用中性色徽标,不使用危险红色;徽标高度不得改变导航行高。搜索框获得内部焦点时显示完整焦点环。 - 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。 - 项目选择器顶行保留项目切换与新建按钮;项目设置统一从下拉列表对应行的管理按钮进入,关闭后焦点返回项目选择器。 +- 项目下拉列表通过 `FloatingPortal` 浮在侧栏与会话区上方,默认宽度 `380px`,不受侧栏裁剪;距窗口边缘至少 `16px`,窄窗口自动收窄,长列表内部滚动。浮层锚定项目选择按钮,窗口、滚动和尺寸变化后重新定位。 - 项目选择器下方仅在有运行中、待处理或已完成未查看会话时显示跨项目活动汇总,无活动时隐藏;项目计数与项目、会话级联菜单遵守 [项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。 活动数量不改变项目菜单的稳定分组和顺序。 diff --git a/package-lock.json b/package-lock.json index 2a2f13b4..5ea09048 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "goodbuddy", - "version": "0.13.12", + "version": "0.13.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goodbuddy", - "version": "0.13.12", + "version": "0.13.13", "license": "0BSD", "dependencies": { "@agentclientprotocol/sdk": "0.25.1", diff --git a/package.json b/package.json index 293e7e7b..b0da0f01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "goodbuddy", - "version": "0.13.12", + "version": "0.13.13", "private": true, "description": "Secure desktop AI workspace with controlled Agent Runtimes", "desktopName": "GoodBuddy", diff --git a/resources/release-notes.json b/resources/release-notes.json index 49f56965..3caf7317 100644 --- a/resources/release-notes.json +++ b/resources/release-notes.json @@ -1,6 +1,34 @@ { "formatVersion": 1, "releases": [ + { + "version": "0.13.13", + "releasedAt": "2026-09-23", + "notes": { + "zh-CN": { + "highlights": [], + "features": [], + "fixes": [ + "**减少流式活动历史的重复读取**:活动历史较多时,连续保存流式更新不再每次重新读取整份历史。保留已有记录与顺序,保存失败后仍可重试。", + "**修复监督反馈侧栏布局**:固定目标与刷新操作使用标题旁的紧凑按钮,目标显示会话或任务名称。长标题和窄侧栏下保持内容可读,并保留键盘焦点提示。" + ], + "notices": [ + "本次仅更新 Desktop,Agent 保持 **0.13.3**,无需因此更新 Host 环境。从 Desktop 0.13.12 升级不新增数据库格式迁移。" + ] + }, + "en-US": { + "highlights": [], + "features": [], + "fixes": [ + "**Reduce repeated activity-history reads during streaming**: With a large activity history, consecutive streaming saves no longer reread the entire history on every update. Existing records and ordering are preserved, and failed saves remain retryable.", + "**Fix the supervision feedback sidebar layout**: Pin and refresh actions now use compact buttons beside the heading, and targets show conversation or task names. Long titles remain readable in narrow sidebars, with visible keyboard focus." + ], + "notices": [ + "This release updates Desktop only. Agent remains at **0.13.3**, with no Host update required for these fixes; upgrading from Desktop 0.13.12 adds no database format migration." + ] + } + } + }, { "version": "0.13.12", "releasedAt": "2026-09-23", diff --git a/src/renderer/src/ProjectSwitcher.test.tsx b/src/renderer/src/ProjectSwitcher.test.tsx index 52807042..684ef4c2 100644 --- a/src/renderer/src/ProjectSwitcher.test.tsx +++ b/src/renderer/src/ProjectSwitcher.test.tsx @@ -354,6 +354,33 @@ describe('ProjectSwitcher project activity integration', () => { }) describe('ProjectSwitcher runtime fields', () => { + it('floats the project menu outside the sidebar and keeps menu interactions open', async () => { + renderSwitcher() + const trigger = screen.getByRole('button', { name: '当前项目' }) + fireEvent.click(trigger) + const menu = screen.getByRole('menu') + expect(trigger.closest('.project-switcher')).not.toContainElement(menu) + expect(menu.closest('.floating-portal')?.parentElement).toBe(document.body) + expect(menu.style.width).toBe('380px') + const selected = within(menu).getByRole('menuitemradio', { checked: true }) + await waitFor(() => expect(selected).toHaveFocus()) + fireEvent.pointerDown(selected) + expect(menu).toBeInTheDocument() + + const width = vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(360) + fireEvent(window, new Event('resize')) + expect(menu.style.width).toBe('328px') + expect(menu.style.left).toBe('16px') + width.mockRestore() + fireEvent.keyDown(selected, { key: 'Escape' }) + expect(screen.queryByRole('menu')).not.toBeInTheDocument() + expect(trigger).toHaveFocus() + + fireEvent.click(trigger) + fireEvent.pointerDown(document.body) + expect(screen.queryByRole('menu')).not.toBeInTheDocument() + }) + it.each(['新建项目', '项目设置'])('keeps %s outside the sidebar and restores focus after closing', (name) => { renderSwitcher() const trigger = screen.getByRole('button', { diff --git a/src/renderer/src/ProjectSwitcher.tsx b/src/renderer/src/ProjectSwitcher.tsx index 1f316a3d..73095f8c 100644 --- a/src/renderer/src/ProjectSwitcher.tsx +++ b/src/renderer/src/ProjectSwitcher.tsx @@ -16,7 +16,7 @@ import { X } from 'lucide-react' import type { LucideIcon } from 'lucide-react' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import type { @@ -57,6 +57,7 @@ import { ProjectWorkModeFields } from './ProjectWorkModeFields' import { ChannelIcon } from './ChannelIcon' import { SegmentedControl } from './WorkspacePrimitives' import { InlineHelp } from './InlineHelp' +import { FloatingPortal } from './FloatingPortal' import { displayErrorMessage } from './error-message' type ProjectSwitcherProps = { @@ -739,6 +740,31 @@ export function ProjectSwitcher({ } } + useLayoutEffect(() => { + if (!projectMenuOpen) return + const anchor = projectPickerButtonRef.current + const menu = projectPickerMenuRef.current + if (!anchor || !menu) return + const position = (): void => { + const rect = anchor.getBoundingClientRect() + const width = Math.min(380, window.innerWidth - 32) + menu.style.width = `${width}px` + menu.style.left = `${Math.max(16, Math.min(rect.left, window.innerWidth - width - 16))}px` + menu.style.top = `${Math.max(16, Math.min(rect.bottom + 8, window.innerHeight - menu.offsetHeight - 16))}px` + } + position() + const observer = new ResizeObserver(position) + observer.observe(anchor) + observer.observe(menu) + window.addEventListener('resize', position) + window.addEventListener('scroll', position, true) + return () => { + observer.disconnect() + window.removeEventListener('resize', position) + window.removeEventListener('scroll', position, true) + } + }, [projectMenuOpen]) + useEffect(() => { if (!projectMenuOpen) { return @@ -757,7 +783,8 @@ export function ProjectSwitcher({ const closeOutside = (event: Event): void => { if ( event.target instanceof Node && - !projectPickerRef.current?.contains(event.target) + !projectPickerRef.current?.contains(event.target) && + !projectPickerMenuRef.current?.contains(event.target) ) { setProjectMenuOpen(false) } @@ -1202,6 +1229,7 @@ export function ProjectSwitcher({