fix: widen project picker beyond the sidebar

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,窄窗口自动收窄。
This commit is contained in:
mesalogo
2026-09-24 00:34:08 +08:00
parent d5f6dc6990
commit 0ed1be1773
7 changed files with 97 additions and 11 deletions
+1
View File
@@ -290,6 +290,7 @@
- 固定导航行高至少 `36px`、行间距 `4px`,图标统一为 `16px`;导航与最近会话分隔线上下各留 `16px`。普通待办和待处理建议数量使用中性色徽标,不使用危险红色;徽标高度不得改变导航行高。搜索框获得内部焦点时显示完整焦点环。
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
- 项目选择器顶行保留项目切换与新建按钮;项目设置统一从下拉列表对应行的管理按钮进入,关闭后焦点返回项目选择器。
- 项目下拉列表通过 `FloatingPortal` 浮在侧栏与会话区上方,默认宽度 `380px`,不受侧栏裁剪;距窗口边缘至少 `16px`,窄窗口自动收窄,长列表内部滚动。浮层锚定项目选择按钮,窗口、滚动和尺寸变化后重新定位。
- 项目选择器下方仅在有运行中、待处理或已完成未查看会话时显示跨项目活动汇总,无活动时隐藏;项目计数与项目、会话级联菜单遵守
[项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。
活动数量不改变项目菜单的稳定分组和顺序。
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+28
View File
@@ -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",
+27
View File
@@ -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', {
+31 -2
View File
@@ -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({
<ChevronDown aria-hidden="true" size={14} />
</button>
{projectMenuOpen && (
<FloatingPortal anchorRef={projectPickerButtonRef}>
<div
aria-label={t('projectSwitcher.selector.ariaLabel')}
className="project-switcher__menu"
@@ -1337,6 +1365,7 @@ export function ProjectSwitcher({
</div>
)}
</div>
</FloatingPortal>
)}
</div>
<button
+7 -6
View File
@@ -2724,18 +2724,19 @@ textarea:focus-visible {
}
.project-switcher__menu {
position: absolute;
z-index: 30;
top: calc(var(--control-height) + var(--space-2));
right: 0;
left: 0;
position: fixed;
-webkit-app-region: no-drag;
z-index: var(--z-dialog);
display: grid;
max-height: min(420px, calc(100vh - 160px));
max-height: min(420px, calc(100dvh - 32px));
max-width: calc(100vw - 32px);
padding: var(--space-2);
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
overflow-y: auto;
background: var(--surface-raised);
color: var(--text-primary);
font-size: var(--font-body);
box-shadow: var(--shadow-dialog);
gap: var(--space-2);
}