diff --git a/docs/features/application-tool-navigation/logic-design.md b/docs/features/application-tool-navigation/logic-design.md
index 9704b771..6c9b53ef 100644
--- a/docs/features/application-tool-navigation/logic-design.md
+++ b/docs/features/application-tool-navigation/logic-design.md
@@ -192,5 +192,5 @@ warming/loading/live/partial/error -> inactive
项目计数。每个项目内先列待处理会话,再列运行中会话,同类别保留来源顺序。
展开项目只改变菜单层级,不导航。选中项目的活动全部结束后返回项目列表;全局活动结束后
-保留已打开的空状态和“暂无活动”入口。后台更新保留仍存在的焦点项,删除焦点项时将焦点
-移至当前层可用项或空菜单。关闭菜单后,新的活动不会自动重新打开菜单。
+关闭菜单并隐藏摘要入口。后台更新保留仍存在的焦点项,删除焦点项时将焦点
+移至当前层可用项。关闭菜单后,新的活动只恢复摘要,不会自动重新打开菜单。
diff --git a/docs/features/application-tool-navigation/prd.md b/docs/features/application-tool-navigation/prd.md
index c0005428..cfcf4e86 100644
--- a/docs/features/application-tool-navigation/prd.md
+++ b/docs/features/application-tool-navigation/prd.md
@@ -142,7 +142,7 @@ Switch,切换后立即持久化。系统工具区域列出四项内置固定
### FR-14 项目活动汇总
用户在任一项目中都能查看跨项目的运行中与待处理会话,并从活动列表直接进入所选会话。
-汇总入口常驻项目选择器下方,没有活动时显示“暂无活动”。点击后先显示有活动会话的项目,
+汇总入口仅在有运行中或待处理会话时显示在项目选择器下方,无活动时整行隐藏。点击后先显示有活动会话的项目,
悬停或点击项目展开会话子菜单,点击会话进入该条对话。项目菜单保留原有分组与顺序,并显示
各项目的活动数量;级联菜单内的项目顺序不随待处理或运行中数量变化。
汇总不受当前项目、搜索条件或最近历史条数限制。计数与排除规则见
diff --git a/docs/features/application-tool-navigation/progress.md b/docs/features/application-tool-navigation/progress.md
index 2c041e7d..0df61dc2 100644
--- a/docs/features/application-tool-navigation/progress.md
+++ b/docs/features/application-tool-navigation/progress.md
@@ -25,9 +25,16 @@ Windows Electron 视觉 fixture 使用当前 `ProjectActivity`、生产样式和
`npm test` 在 240 秒后超时,超时前 `tests/agent-package.test.ts` 的
`verifies and installs offline dependency inventories larger than one MiB` 用例失败,未取得完整结果。
-Demo 已按最新讨论隐藏空闲摘要并移除会话行箭头;生产实现仍保留空闲摘要,尚未与 Demo 对齐。
+Demo 已按最新讨论隐藏空闲摘要并移除会话行箭头;生产空闲摘要在下述非驻留修正中对齐。
后续界面修正已移除生产会话行箭头,并为活动摘要与下方新建会话增加 `8px` 间距。
+## 2026-09-14:摘要非驻留修正
+
+生产 `ProjectActivity` 在无活动时返回空内容,活动归零时关闭级联菜单;新活动只恢复摘要。
+组件回归 11 项通过,覆盖初始空闲、活动归零关闭菜单及活动恢复后不自动展开。
+App 的 `project activity integration` 定向回归 18 项通过,类型检查与 Lint 通过。
+本轮 `npm test` 在 60 秒后超时,未取得全量结果;未重新执行 Electron 视觉验证。
+
## 2026-09-14:0.13.2 候选验证
正式发布准备时再次复现 App 问答用例的时序失败。活动摘要改为常驻后,等待该入口出现
diff --git a/docs/features/application-tool-navigation/ui-design.md b/docs/features/application-tool-navigation/ui-design.md
index 390ff3f1..79a6ce39 100644
--- a/docs/features/application-tool-navigation/ui-design.md
+++ b/docs/features/application-tool-navigation/ui-design.md
@@ -19,7 +19,7 @@
```text
品牌
项目选择
-项目活动汇总(常驻,空闲显示“暂无活动”)
+项目活动汇总(仅有活动时显示)
新对话
搜索
@@ -209,7 +209,7 @@ CPU 4% · 内存 620 MB · 桌面进程 7
对应 [FR-14](./prd.md#fr-14-项目活动汇总),状态与计数遵守
[活动逻辑](./logic-design.md#9-项目活动汇总)。
-- 全局汇总按钮常驻项目选择器下方,紧凑显示待处理与运行中数量,空闲显示“暂无活动”,
+- 全局汇总按钮仅在有活动时显示在项目选择器下方,紧凑显示待处理与运行中数量,空闲时整行隐藏,
右侧显示展开箭头,与下方新建会话保留 `8px` 间距。可访问名称明确数量属于“全项目活动”。
- 项目菜单在项目行显示同口径数量,保留“本地项目 / 远程项目 / 远程通道”分组和 SSH Host
层级,不因活动状态重排项目。数量为零时不显示标记,连接状态仍独立显示。
diff --git a/docs/features/application-tool-navigation/user-stories.md b/docs/features/application-tool-navigation/user-stories.md
index 59ade905..f32ececa 100644
--- a/docs/features/application-tool-navigation/user-stories.md
+++ b/docs/features/application-tool-navigation/user-stories.md
@@ -260,6 +260,6 @@
- Given 鼠标从项目行移向相接的会话面板
- Then 子菜单保持打开,点击会话正常导航
- Given 当前项目的活动全部结束
-- Then 返回项目层并恢复可用焦点;全局无活动时摘要显示“暂无活动”,已打开菜单显示空状态
+- Then 返回项目层并恢复可用焦点;全局无活动时整行隐藏摘要并关闭菜单,新活动到来时只恢复摘要
- When 点击外部、焦点离开、按 Tab 或隐藏侧栏
- Then 菜单关闭,后台新活动不自动重新展开
diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx
index 4288a7d1..28050c17 100644
--- a/src/renderer/src/App.test.tsx
+++ b/src/renderer/src/App.test.tsx
@@ -1285,9 +1285,8 @@ describe("App", () => {
vi.mocked(api.tasks.list).mockResolvedValue([{ ...task, status: "completed" }]);
act(() => conversationQueueChangeListener?.(task.conversationId!));
- await waitFor(() => expect(summary).toHaveTextContent("暂无活动"));
- expect(screen.getByRole("menu", { name: "全项目活动" })).toHaveTextContent("暂无运行中或待处理会话");
- fireEvent.keyDown(document.activeElement!, { key: "Escape" });
+ await waitFor(() => expect(summary).not.toBeInTheDocument());
+ expect(screen.queryByRole("menu", { name: "全项目活动" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "当前项目" }));
expect(within(screen.getByRole("menu")).getByRole("menuitemradio", { name: /Background project/u }))
.not.toHaveTextContent("个运行中");
@@ -1517,8 +1516,8 @@ describe("App", () => {
: { requestId: request.requestId, type: "error", status: "failed", message: "Activity execution failed" });
conversationQueueChangeListener?.(request.conversationId!);
});
- await waitFor(() => expect(screen.getByRole("button", { name: /全项目活动/u })).toHaveTextContent("暂无活动"));
- expect(menu).toHaveTextContent("暂无运行中或待处理会话");
+ await waitFor(() => expect(screen.queryByRole("button", { name: /全项目活动/u })).not.toBeInTheDocument());
+ expect(menu).not.toBeInTheDocument();
});
});
diff --git a/src/renderer/src/ProjectActivity.test.tsx b/src/renderer/src/ProjectActivity.test.tsx
index def44158..4b6d3a4c 100644
--- a/src/renderer/src/ProjectActivity.test.tsx
+++ b/src/renderer/src/ProjectActivity.test.tsx
@@ -37,13 +37,10 @@ function open(): HTMLButtonElement {
afterEach(() => { cleanup(); vi.restoreAllMocks() })
describe('ProjectActivity', () => {
- it('keeps an idle summary and an accessible empty menu while hiding zero counts', () => {
+ it('hides the idle summary and zero counts', () => {
render(view([]))
- const trigger = open()
- expect(trigger).toHaveTextContent('No activity')
- expect(screen.getByRole('menu')).toHaveTextContent('No running conversations')
- fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
- expect(trigger).toHaveFocus()
+ expect(screen.queryByRole('button', { name: /All project activity/ })).not.toBeInTheDocument()
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument()
const { container } = render()
expect(container).toBeEmptyDOMElement()
})
@@ -165,10 +162,10 @@ describe('ProjectActivity', () => {
rerender(view(activities.filter((activity) => activity.projectId === 'local')))
expect(screen.getByRole('menuitem', { name: /Local/ })).toHaveFocus()
rerender(view([]))
- expect(screen.getByRole('menu')).toHaveTextContent('No running conversations')
- fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
+ expect(screen.queryByRole('button', { name: /All project activity/ })).not.toBeInTheDocument()
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
rerender(view())
+ expect(screen.getByRole('button', { name: /All project activity/ })).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
})
diff --git a/src/renderer/src/ProjectActivity.tsx b/src/renderer/src/ProjectActivity.tsx
index 38e508dc..cb747472 100644
--- a/src/renderer/src/ProjectActivity.tsx
+++ b/src/renderer/src/ProjectActivity.tsx
@@ -227,15 +227,16 @@ export function ProjectActivity({
const [open, setOpen] = useState(false)
const triggerRef = useRef(null)
const id = useId()
- if (!visible && open) setOpen(false)
+ if ((!visible || activities.length === 0) && open) setOpen(false)
const running = useMemo(() => activities.filter((activity) => activity.status === 'running').length, [activities])
+ if (activities.length === 0) return null
return (
<>
{open && visible && (