fix: distinguish Apps and Settings in the sidebar footer

Keep the horizontal footer layout with visible labels, matching button heights, a subtle divider, and an Apps dropdown indicator. Use a layout icon for Manage Apps so it is distinct from global Settings.

Release note: Make the bottom-left Apps and Settings entries easier to distinguish with labeled buttons and separate visual boundaries.
This commit is contained in:
mesalogo
2026-09-28 18:08:52 +08:00
parent c11f204b17
commit 8650907b75
14 changed files with 71 additions and 45 deletions
+3 -2
View File
@@ -141,10 +141,11 @@ records are listed separately and do not introduce another feature status.
models, approval policy, budgets, and timeouts in a Project, with reusable
templates.
- [x] **Application center and navigation**:
Clicking the bottom App Center opens a lightweight upward anchored popup with enabled apps,
The footer shows equal-height Apps and Settings buttons with visible labels and a subtle vertical divider.
Clicking Apps, marked with a downward chevron, opens a lightweight upward anchored popup with enabled apps,
regardless of pinning or opening history, and no modal backdrop. Clicking an app row closes
the popup; Local Inference Monitor opens a separate modal preserving the workspace, while other apps
open their main content pages. Manage Apps opens searchable cards with settings details;
open their main content pages. Manage Apps uses a LayoutGrid icon and opens Application Center with searchable cards and settings details;
layout and interaction rules are defined in the [UI design](./docs/features/application-tool-navigation/ui-design.md).
All four application cards support arrow and drag reordering regardless of enablement or pinning.
Cards, the launcher, and the filtered sidebar share one persisted order. Knowledge
+1 -1
View File
@@ -36,7 +36,7 @@
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
- [x] **助手工作栏、多终端与可调布局**:工作栏开关位于明暗主题按钮旁;右侧工作栏使用持久“+”能力目录和应用 Tab,任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目和所有项目,范围随布局保存;无项目任务在所有项目中标注“未绑定项目”。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记编辑器的 AI 评论栏支持鼠标与键盘调宽;AI 评论栏记住显示状态及宽度,窄窗口下移至编辑器底部。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
- [x] **应用中心与导航**:底部应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻筛选。点击应用行关闭菜单,本机推理监控打开保留底层工作区的独立 Modal,其余应用打开主内容区;底部“管理应用”打开可搜索卡片及设置详情,布局与交互见 [UI 设计](./docs/features/application-tool-navigation/ui-design.md)。四张应用卡片均支持上下移动和拖动排序,不受启用或常驻状态限制;卡片、菜单和经过过滤的侧栏共用一份保存顺序。知识库始终启用并常驻,提供“打开”和排序;监督者、魔法笔记和本机推理监控可设置启用与常驻。监督者沿用智能心跳身份,缺失启用设置时默认关闭,已明确保存的选择保留;关闭后保留计划和历史,拒绝新回顾并隐藏侧栏反馈,在途运行可完成。启用应用不会自动创建计划。旧应用顺序补齐默认项并保留已有相对顺序。设置持久化后的事件同步各入口,包括工具配置写入;重开中心刷新期间锁定修改,新快照覆盖迟到的旧读取。`local-inference` Modal 直接显示服务列表,不展示任务历史或外部连接;向量服务显示独立进程 CPU 与工作集内存,ASR/OCR 说明共享进程无法独立统计的原因。ASR 就绪状态未知,不从活动请求推断运行中。受支持的服务操作保留影响确认,失败后刷新列表并要求重新确认;TTS 显示不可用。取消中的推理在 Worker 确认或退出前仍计入活动任务及服务停止影响。实现已接入生产 App;验收记录分别列出完整 App、组件 fixture、真实本地引擎和剩余平台/安装包覆盖,不以历史失败代替当前全量结果,详见[验证进度](./docs/features/application-tool-navigation/progress.md)。
- [x] **应用中心与导航**:底部“应用”和“设置”为显示文字的等高按钮,中间使用弱竖分隔线。“应用”带向下箭头,点击后向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻筛选。点击应用行关闭菜单,本机推理监控打开保留底层工作区的独立 Modal,其余应用打开主内容区;底部“管理应用”使用 `LayoutGrid` 图标,打开标题仍为“应用中心”的可搜索卡片及设置详情,布局与交互见 [UI 设计](./docs/features/application-tool-navigation/ui-design.md)。四张应用卡片均支持上下移动和拖动排序,不受启用或常驻状态限制;卡片、菜单和经过过滤的侧栏共用一份保存顺序。知识库始终启用并常驻,提供“打开”和排序;监督者、魔法笔记和本机推理监控可设置启用与常驻。监督者沿用智能心跳身份,缺失启用设置时默认关闭,已明确保存的选择保留;关闭后保留计划和历史,拒绝新回顾并隐藏侧栏反馈,在途运行可完成。启用应用不会自动创建计划。旧应用顺序补齐默认项并保留已有相对顺序。设置持久化后的事件同步各入口,包括工具配置写入;重开中心刷新期间锁定修改,新快照覆盖迟到的旧读取。`local-inference` Modal 直接显示服务列表,不展示任务历史或外部连接;向量服务显示独立进程 CPU 与工作集内存,ASR/OCR 说明共享进程无法独立统计的原因。ASR 就绪状态未知,不从活动请求推断运行中。受支持的服务操作保留影响确认,失败后刷新列表并要求重新确认;TTS 显示不可用。取消中的推理在 Worker 确认或退出前仍计入活动任务及服务停止影响。实现已接入生产 App;验收记录分别列出完整 App、组件 fixture、真实本地引擎和剩余平台/安装包覆盖,不以历史失败代替当前全量结果,详见[验证进度](./docs/features/application-tool-navigation/progress.md)。
- [ ] **私有化应用市场**(规划中):在应用管理基础上提供组织自己的目录与应用分发。当前只管理内置应用,尚无市场浏览或安装能力,详见 [FR-15](./docs/features/application-tool-navigation/prd.md#fr-15-私有化市场与-yaml-交换后续)。
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入更完整的执行监督、统一 Runtime 监控、受管进程、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
- [ ] **ShareServer Office 协同编辑**(设计中):通过可选 ShareServer 集成 ONLYOFFICE Docs,在助手工作栏中以文件名打开多个文档 Tab,支持 DOCX、XLSX、PPTX 的人工编辑、保存、撤销、源文件冲突保护和后续 AI 选区修改。Office 正文会由所选 ShareServer 和编辑引擎处理;Desktop 不内置或启动 Document Server,未配置服务时不宣称离线编辑。详见[设计文档](./docs/features/office-document-editing/README.md)。
+1 -1
View File
@@ -329,7 +329,7 @@
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
单行截断,不能撑宽或挤压侧栏。
- 侧栏底部提供应用中心入口,设置按钮独立靠右。点击应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选;点击行关闭菜单,知识库、智能心跳和魔法笔记在主内容区打开页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。菜单底部“管理应用”打开既有整窗管理 Modal,共同应用设置沿用设置 Modal。
- 侧栏底部提供“应用 / Apps”和“设置 / Settings”两个横向等高 `nav-item` 按钮,均显示图标和文字,中间使用弱竖分隔线。“应用”带向下箭头,点击后向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选;点击行关闭菜单,知识库、智能心跳和魔法笔记在主内容区打开页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。菜单底部“管理应用”使用 `LayoutGrid` 图标,打开既有整窗管理 Modal,标题仍为“应用中心 / Application Center”,共同应用设置沿用设置 Modal。
四个内置应用的管理列表、菜单和侧栏共用保存顺序,菜单按启用过滤,侧栏按启用且常驻过滤。知识库、智能心跳始终启用并常驻,可调整位置;管理 Modal 提供“打开”和排序,不提供启用、常驻开关或通用空设置,标签为“始终显示”(英文 `Always shown`)。原工作区配置及心跳单条计划 enabled 保持不变。只有魔法笔记、本机推理监控可修改启用和常驻,取消常驻不关闭功能。当前对话、运行记录和设置保持固定系统位置,不参与应用排序。
设置持久化后的事件同步各入口及工具配置写入;重开中心刷新期间锁定修改,新快照不被旧读取覆盖。文件、终端、浏览器和资源监控只通过右侧工作栏访问,不在左侧复制入口。
管理 Modal 始终使用紧凑单列横向列表,纵向对应保存顺序;左侧为图标、名称及简短用途,右侧为打开、可用的设置及上下移动操作,行间使用弱分隔线,不加卡片外框。窄窗口操作换行且不溢出,保留共享键盘焦点环。可选应用关闭时禁用“打开”。启用、常驻位于紧凑设置详情;四行均提供上下移动及拖动排序,不受启用或常驻影响,箭头按完整顺序判断边界。搜索时禁用拖动,箭头仍按完整顺序移动,不丢失未命中项。尺寸、返回与滚动重置规则见[应用中心 UI 设计](./docs/features/application-tool-navigation/ui-design.md)。已有稳定设置入口时,不在顶栏重复提供同一入口。
@@ -1,7 +1,7 @@
# 应用导航与系统工具入口
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,四个内置应用共用可调整的保存顺序,知识库始终启用并常驻;
智能心跳、魔法笔记和本机推理监控可通过应用管理控制启用和常驻。底部应用中心向上展开轻量锚定菜单,列出已启用应用;底部“管理应用”打开既有管理 Modal。系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
智能心跳、魔法笔记和本机推理监控可通过应用管理控制启用和常驻。底部“应用”向上展开轻量锚定菜单,列出已启用应用;底部“管理应用”打开既有“应用中心”管理 Modal。系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
商店入口、推荐位或安装流程。
当前分期见 [PRD 第 8 节](./prd.md#8-分期),实施步骤见[技术设计第 11 节](./technical-design.md#11-实施顺序)。
@@ -86,7 +86,7 @@
### FR-5 应用中心入口
侧栏底部提供应用中心和独立设置入口。点击应用中心向上展开锚定按钮的轻量菜单,无模态遮罩。菜单列出已启用应用,不按是否打开过、历史记录或常驻状态筛选;点击应用行关闭菜单,本机推理监控打开 Modal,其余应用在主内容区打开页面。
侧栏底部提供“应用”(英文 `Apps`)和独立“设置”入口,两个按钮均显示文字。点击“应用”向上展开锚定按钮的轻量菜单,无模态遮罩。菜单列出已启用应用,不按是否打开过、历史记录或常驻状态筛选;点击应用行关闭菜单,本机推理监控打开 Modal,其余应用在主内容区打开页面。
菜单底部“管理应用”关闭菜单并打开既有管理 Modal,在其中浏览、搜索全部内置应用,为已关闭的可选应用提供设置与重新开启入口。无搜索结果时显示空状态并可清除搜索。
首期不展示商店、安装、推荐或系统工具清单。入口具有可访问名称,Escape 关闭后恢复触发位置的焦点。
@@ -175,17 +175,21 @@ Main 继续校验可信 sender、Zod 输入,复用 `ApplicationSettingsStore`
原 `user-card` 整行设置按钮已替换为容器和相邻独立按钮:
```tsx
<div className="sidebar-footer">
<button onClick={toggleApplicationMenu} aria-haspopup="menu" aria-expanded={applicationMenuOpen} ...>
{t('navigation.applicationCenter')}
<div className="sidebar-footer sidebar-footer--applications">
<button className="nav-item" onClick={toggleApplicationMenu} aria-haspopup="menu" aria-expanded={applicationMenuOpen} ...>
<Grid2X2 aria-hidden="true" />
<span>{t('applications.menuLabel')}</span>
<ChevronDown aria-hidden="true" />
</button>
<button aria-label={t('navigation.settings')} ...>
<div className="sidebar-footer__divider" aria-hidden="true" />
<button className="nav-item" aria-label={t('navigation.settings')} ...>
<Settings aria-hidden="true" />
<span>{t('navigation.settings')}</span>
</button>
</div>
```
应用中心和设置为相邻独立按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
“应用”(`applications.menuLabel`:中文“应用”、英文 `Apps`)和设置为横向等高独立按钮,使用 `nav-item` 样式和弱竖分隔线;“管理应用”菜单项使用 `LayoutGrid` 图标,管理 Modal 标题保持“应用中心” / `Application Center`。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
保留设置预加载、焦点恢复和未保存离开确认。该调用只打开设置 Modal,不更新当前工作区或
KeepAlive 缓存;`settingsOpen` 独立管理打开状态。关闭后卸载设置内容,下次按入口指定的分类
和消息通道重新挂载,避免缓存上次分类覆盖本次直达目标。底层工作区继续挂载,普通关闭不跳回聊天。
@@ -32,7 +32,7 @@
...
────────────────────────
[应用中心] [设置]
[应用 v] | [设置]
```
“当前对话”和“运行记录”属于固定系统入口,常驻应用位于二者之间。首期应用清单为魔法笔记、
@@ -41,15 +41,15 @@
## 2. 底部应用中心入口
底部使用连续平整表面和顶部分隔线,应用中心在前,固定设置靠右,二者为独立按钮:
底部使用连续平整表面和顶部分隔线,“应用”在前,“设置”靠右,二者为横向排列的等高 `nav-item` 独立按钮,中间使用弱竖分隔线:
| 位置 | 图标 | 可见工具提示 | 点击结果 |
| 位置 | 图标 | 可见文字 | 点击结果 |
| --- | --- | --- | --- |
| 1 | `Grid2X2` | 应用中心 | 向上展开已启用应用菜单,底部“管理应用”进入管理 Modal |
| 末位 | `Settings` | 设置 | 在当前工作区上打开设置 Modal |
| 1 | `Grid2X2` + `ChevronDown` | 应用 / Apps | 向上展开已启用应用菜单,底部“管理应用”进入管理 Modal |
| 末位 | `Settings` | 设置 / Settings | 在当前工作区上打开设置 Modal |
图标使用项目现有 Lucide 库。应用中心保留文字,设置图标采用统一的 `34px × 34px` 点击区域。
底部使用共享间距和焦点样式,设置通过弹性空白靠右;不得把整个容器做成设置按钮。
图标使用项目现有 Lucide 库。两个按钮均保留可见文字,沿用主导航的高度、间距和焦点样式;
“应用”文字后显示向下箭头,菜单仍向上展开。不得把整个容器做成设置按钮。
### 2.1 状态
@@ -84,7 +84,7 @@
菜单通过共享 `AnchoredMenu` 挂载到 body,宽度最多 `280px`,距窗口边缘至少 `16px`,与按钮上缘间隔 `8px`;短窗口限制高度并内部滚动,滚动、窗口和内容尺寸变化时重算位置。原生浏览器相交遮挡复用共享机制。
管理 Modal 沿用设置 Modal 的表面、遮罩和焦点规则,提供全部内置应用的名称及用途搜索、打开,以及可选应用的常驻和设置:
“管理应用”菜单项使用 `LayoutGrid` 图标。管理 Modal 标题仍为“应用中心”(英文 `Application Center`),沿用设置 Modal 的表面、遮罩和焦点规则,提供全部内置应用的名称及用途搜索、打开,以及可选应用的常驻和设置:
应用管理始终采用紧凑单列横向列表,从上到下对应保存顺序;行间使用 `--border-subtle` 弱分隔线,不加卡片外框。概览与设置详情共用 `640px` 最大宽度,窄窗口保留两侧各 `16px` 外边距,高度随内容增长并受窗口限制。
每行左侧显示图标、名称、状态和简短用途,右侧对齐打开、可用的设置及上下移动操作;空间不足时操作组换到下一行并保持右对齐,组内也可换行,上下箭头保持成组。名称旁区分
@@ -189,7 +189,8 @@ CPU 4% · 内存 620 MB · 桌面进程 7
| 对象 | 文案 |
| --- | --- |
| 底部入口 | 应用中心 |
| 底部入口 | 应用 / Apps |
| 管理 Modal 标题 | 应用中心 / Application Center |
| 菜单底部管理入口 | 管理应用 |
| 应用设置开关 | 启用应用 |
| 文件系统工具 | 文件 |
@@ -42,7 +42,7 @@
- Given 任意主页面和任意侧栏宽度
- When 用户查看侧栏底部
- Then 设置与应用中心为始终可见的独立按钮,点击区域不重叠
- Then “设置”与“应用”为始终显示文字的独立按钮,点击区域不重叠
- And 点击后继续遵守未保存内容的离开确认
## 3. Epic B:管理应用入口
+21 -18
View File
@@ -6204,7 +6204,7 @@ describe("App", () => {
name: "选择知识库,本次已启用 1 个",
}),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '应用中心' }));
fireEvent.click(screen.getByRole('button', { name: '应用' }));
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }));
fireEvent.change(screen.getByRole('searchbox'), { target: { value: '知识库' } });
expect(within(screen.getByRole('dialog')).queryByRole('switch')).not.toBeInTheDocument();
@@ -13904,7 +13904,7 @@ describe("App", () => {
expect(screen.queryByRole("button", { name: "应用设置" })).not.toBeInTheDocument();
expect(api.updates.updateSettings).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "关闭设置" }));
fireEvent.click(screen.getByRole("button", { name: "应用中心" }));
fireEvent.click(screen.getByRole("button", { name: "应用" }));
fireEvent.click(screen.getByRole("menuitem", { name: "管理应用" }));
fireEvent.click(screen.getByRole("button", { name: "魔法笔记 应用设置" }));
const toggle = await screen.findByRole("switch", {
@@ -13942,7 +13942,7 @@ describe("App", () => {
await updates.updateSettings({ magicNotesEnabled: true })
render(<App />)
await screen.findByRole('button', { name: '魔法笔记' })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
const move = screen.getByRole('button', { name: '下移 知识库' })
await waitFor(() => expect(move).toBeEnabled())
@@ -13962,7 +13962,7 @@ describe("App", () => {
expect(within(screen.getByRole('navigation', { name: '主导航' })).getAllByRole('button').map(button => button.textContent)).toEqual([
'对话', '监督者', '知识库', '魔法笔记', '运行记录',
])
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
expect(screen.getAllByRole('menuitem').map(item => item.textContent)).toEqual(['监督者', '本机推理监控', '知识库', '魔法笔记', '管理应用'])
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
expect(within(screen.getByRole('dialog', { name: '应用中心' })).getAllByRole('article').map(card => card.querySelector('strong')?.textContent)).toEqual(['监督者', '本机推理监控', '知识库', '魔法笔记'])
@@ -13982,7 +13982,7 @@ describe("App", () => {
},
}
vi.mocked(updates.getSettings).mockResolvedValue({ ...external, applicationNavigation: { ...external.applicationNavigation, order: [...external.applicationNavigation.order] } })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
const pin = screen.getByRole('switch', { name: '常驻左侧菜单' })
@@ -14014,7 +14014,7 @@ describe("App", () => {
await screen.findByRole('button', { name: '监督者' })
const reply = deferred<ApplicationSettings>()
if (operation === 'read') vi.mocked(updates.getSettings).mockReturnValueOnce(reply.promise)
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
const pin = screen.getByRole('switch', { name: '常驻左侧菜单' })
@@ -14181,7 +14181,7 @@ describe("App", () => {
fireEvent.click(screen.getByRole('button', { name: /^笔记搜索/ }))
fireEvent.click(await screen.findByRole('button', { name: '新建笔记' }))
fireEvent.change(screen.getByLabelText('笔记标题'), { target: { value: 'Unsaved quick note' } })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
const center = await screen.findByRole('dialog', { name: '应用中心' })
fireEvent.click(within(center).getByRole('button', { name: /魔法笔记.*设置|设置.*魔法笔记/ }))
@@ -14235,7 +14235,7 @@ describe("App", () => {
expect(await screen.findByRole('alertdialog')).toBeVisible()
expect(screen.queryByRole('button', { name: '关闭设置' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '继续编辑' }))
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '知识库' }))
expect(await screen.findByRole('alertdialog')).toBeVisible()
fireEvent.click(screen.getByRole('button', { name: '放弃草稿并切换' }))
@@ -14263,7 +14263,7 @@ describe("App", () => {
if (entry === 'sidebar') await api.updates!.updateSettings({ applicationNavigation: { ...defaultTestApplicationNavigation, pinned: { ...defaultTestApplicationNavigation.pinned, 'local-inference': true } } })
render(<App />)
await waitFor(() => expect(api.updates!.getSettings).toHaveBeenCalled())
const nav = entry === 'sidebar' ? await screen.findByRole('button', { name: '本机推理监控' }) : screen.getByRole('button', { name: '应用中心' })
const nav = entry === 'sidebar' ? await screen.findByRole('button', { name: '本机推理监控' }) : screen.getByRole('button', { name: '应用' })
if (entry !== 'sidebar') expect(within(screen.getByRole('navigation', { name: '主导航' })).getAllByRole('button').map(button => button.textContent)).toEqual(['对话', '知识库', '监督者', '运行记录'])
fireEvent.click(screen.getByRole('button', { name: '知识库' }))
const workspace = await screen.findByLabelText('知识工作区')
@@ -14271,7 +14271,7 @@ describe("App", () => {
nav.focus()
fireEvent.click(nav)
} else {
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
if (entry === 'menu') fireEvent.click(screen.getByRole('menuitem', { name: '本机推理监控' }))
else {
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
@@ -14290,7 +14290,7 @@ describe("App", () => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.getByLabelText('知识工作区')).toBe(workspace)
expect(workspace.closest('.app-shell')).toHaveProperty('inert', false)
expect(entry === 'sidebar' ? nav : screen.getByRole('button', { name: '应用中心' })).toHaveFocus()
expect(entry === 'sidebar' ? nav : screen.getByRole('button', { name: '应用' })).toHaveFocus()
})
it('saves Supervisor timeouts and concurrency through application settings and restores them on reopen', async () => {
@@ -14342,7 +14342,7 @@ describe("App", () => {
render(<App />)
expect(await screen.findByRole('button', { name: '监督者' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '监督者 应用设置' }))
const enable = screen.getByRole('switch', { name: '启用应用' })
@@ -14362,7 +14362,7 @@ describe("App", () => {
{ name: '监督者' },
),
).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
expect(screen.queryByRole('menuitem', { name: '监督者' })).not.toBeInTheDocument()
expect(api.heartbeats.remove).not.toHaveBeenCalled()
expect(await api.heartbeats.list()).toEqual([plan])
@@ -14377,7 +14377,7 @@ describe("App", () => {
})
render(<App />)
const inferenceNav = await screen.findByRole('button', { name: '本机推理监控' })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
const pin = screen.getByRole('switch', { name: '常驻左侧菜单' })
@@ -14385,11 +14385,14 @@ describe("App", () => {
fireEvent.click(pin)
await waitFor(() => expect(inferenceNav).not.toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '关闭应用中心' }))
const trigger = screen.getByRole('button', { name: '应用中心' })
const trigger = screen.getByRole('button', { name: '应用', expanded: false })
expect(trigger).toHaveFocus()
expect(within(screen.getByRole('button', { name: '设置' })).getByText('设置')).toBeVisible()
fireEvent.click(trigger)
expect(trigger).toHaveAttribute('aria-expanded', 'true')
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('menuitem', { name: '本机推理监控' }))
expect(trigger).toHaveAttribute('aria-expanded', 'false')
const workspace = await screen.findByRole('region', { name: '本机推理服务' })
expect(within(screen.getByRole('navigation', { name: '主导航' })).queryByRole('button', { name: '本机推理监控' })).not.toBeInTheDocument()
await act(async () => changed(await api.updates!.updateSettings({ localInferenceEnabled: false })))
@@ -14403,14 +14406,14 @@ describe("App", () => {
expect((await api.updates!.getSettings()).applicationNavigation.pinned['local-inference']).toBe(false)
await act(async () => changed(await api.updates!.updateSettings({ localInferenceEnabled: false })))
fireEvent.click(screen.getByRole('button', { name: '关闭本机推理监控' }))
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
await waitFor(() => expect(screen.getByRole('switch', { name: '启用应用' })).not.toBeDisabled())
fireEvent.click(screen.getByRole('switch', { name: '启用应用' }))
await waitFor(() => expect(screen.getByRole('switch', { name: '启用应用' })).toBeChecked())
fireEvent.click(screen.getByRole('button', { name: '关闭应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '本机推理监控' }))
expect(await screen.findByRole('region', { name: '本机推理服务' })).toBeVisible()
})
@@ -14418,7 +14421,7 @@ describe("App", () => {
it('keeps confirmed application preferences and locks edits until an unknown save is read back', async () => {
render(<App />)
await screen.findByRole('button', { name: '知识库' })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
const updates = api.updates!
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
+6 -3
View File
@@ -9362,10 +9362,12 @@ function App(): React.JSX.Element {
}}
>
<Grid2X2 size={17} aria-hidden="true" />
<span>{t("applications.title")}</span>
<span>{t("applications.menuLabel")}</span>
<ChevronDown size={16} aria-hidden="true" />
</button>
<div className="sidebar-footer__divider" aria-hidden="true" />
<button
className="icon-button"
className="nav-item"
aria-label={t('navigation.settings')}
title={t('navigation.settings')}
type="button"
@@ -9375,7 +9377,8 @@ function App(): React.JSX.Element {
}
onPointerEnter={() => preloadWorkspaceRouteOnIntent("settings")}
>
<Settings size={16} />
<Settings size={16} aria-hidden="true" />
<span>{t('navigation.settings')}</span>
</button>
</div>
</aside>
+2 -2
View File
@@ -1,5 +1,5 @@
import { useMemo, type RefObject } from 'react'
import { Settings } from 'lucide-react'
import { LayoutGrid } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { defaultApplicationNavigation, type ApplicationSettings, type BuiltInApplicationId } from '../../shared/application-settings-contracts'
import { applicationDefinitions, isApplicationEnabled } from './ApplicationCenter'
@@ -36,7 +36,7 @@ export function ApplicationMenu({ anchorRef, settings, pending, error, onClose,
})}
<div role="separator" />
<button type="button" role="menuitem" tabIndex={-1} onClick={() => { onClose(); anchorRef.current?.focus(); onManage() }}>
<Settings size={16} aria-hidden="true" /><span>{t('applications.manage')}</span>
<LayoutGrid size={16} aria-hidden="true" /><span>{t('applications.manage')}</span>
</button>
</AnchoredMenu>
}
+13 -1
View File
@@ -205,5 +205,17 @@
gap: var(--space-2);
}
.sidebar-footer--applications > .nav-item {
flex: 1;
flex: 1 0 auto;
gap: var(--space-2);
padding-inline: var(--space-2);
justify-content: center;
white-space: nowrap;
}
.sidebar-footer--applications > .nav-item > span {
flex: 0 0 auto;
}
.sidebar-footer__divider {
flex: 0 0 1px;
height: var(--space-4);
background: var(--border-subtle);
}
@@ -9,6 +9,7 @@ export const app = {
priority: { low: 'Low priority', medium: 'Medium priority', high: 'High priority' }
},
applications: {
menuLabel: 'Apps',
alwaysShown: 'Always shown', optional: 'Optional',
manage: 'Manage Apps',
title: 'Application Center', description: 'Open and reorder built-in applications, and manage optional apps and pins. Knowledge is always shown; Supervisor can be enabled or disabled.',
@@ -6,6 +6,7 @@ export const app = {
priority: { low: '低优先级', medium: '中优先级', high: '高优先级' }
},
applications: {
menuLabel: '应用',
alwaysShown: '始终显示', optional: '可选',
manage: '管理应用',
title: '应用中心', description: '打开和排列内置应用,管理可选应用的启用与常驻。知识库始终显示,监督者可启用或关闭。',