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({
{projectMenuOpen && (
+
)}
+
)}