${item.builtin ? '内置应用' : '商店应用 · 示例'}
${variant === 'cards' ? item.summary : item.detail}
diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 5b31d50e..ba887f83 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -605,7 +605,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - 模式、模型或工具权限属于上下文控制,不与页面导航页签混用。 - 模型、专家角色、工作模式、OpenCode Agent、Continue 预设和 Runtime 快捷操作使用统一的上下文单选菜单,并保持菜单互斥、键盘可达和选中状态明确。 - Runtime 选择菜单必须反映当前执行空间的真实能力。托管 SSH 项目当前只支持 OpenCode,因此输入区只渲染已配置的 OpenCode 选项和管理入口,不显示直连模型、Continue 或 DeepSeek Harness 的不可用入口;旧会话保存的其他 Runtime 选择在项目激活时恢复为当前 OpenCode 配置。 -- 输入区第一行工具栏只承载附件、语音、知识范围、专家角色、工作模式、Runtime 选择和发送等通用操作。OpenCode Agent、Continue 预设及 Runtime 快捷操作必须放入其下方独立的 Runtime 专属功能行,通过可见分组名称、顶部边界和差异化表面与通用操作分层;该行只承载对当前消息生效的高频选择,当前 Runtime 没有可选专属功能时不保留空行。 +- 输入区使用一个完整容器,主工具栏保留附件、语音、Runtime、工作模式、会话选项及停止/发送。知识范围、专家角色、OpenCode Agent、Continue 预设和 Runtime 快捷操作收纳在会话选项浮层中;Runtime 专属选择在浮层内按提供方分组,无可选功能时不保留空分组。输入框上方以简短摘要显示生效的专家、知识范围、检索方式和 Runtime 专属选择。 +- 会话选项浮层与主行菜单互斥,内部复用上下文选择菜单;子菜单就地展开,浮层统一限制宽高并滚动。Escape 先关闭子菜单并恢复其触发按钮焦点,再关闭会话选项并恢复入口焦点;点击外部、焦点离开、切换会话或开始/结束回复时关闭浮层。 +- 会话选项入口紧随录音键,使用无常驻边框的调节图标,保留可访问名称、悬停说明和展开态反馈;浮层在工具栏左侧展开,最大宽度为 `300px`,窄窗口不超过输入区。浮层内选择器使用整行轻量列表样式,避免重复的按钮边框;知识库行右侧显示展开箭头,展开时朝上。 - OpenCode、Continue 和 DeepSeek Harness 后续的 Task 级委派与取消、后台执行进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止及原生会话监督统一进入应用级助手工作栏固定的“Runtime”栏目,不加入 Composer。栏目入口始终存在;只选择 Conversation 或 Task,并按 Task 聚合执行状态,不显示 Job/Run 树或独立操作菜单。内部可选区域按用户所选目标和 Runtime 的真实能力显示,不为未支持能力渲染空卡片或成排禁用按钮。切换跟随目标时必须清理上一归属的监督状态,固定目标则保持不变。 - 设置中心只管理持久 Runtime 配置、默认值和能力清单;右侧 Runtime 栏目管理用户当前跟随或固定目标的生命周期。两处不得复制同一实时操作,栏目中的高风险操作仍须就地确认并保留取消、权限、用量和活动审计。 - Runtime Prompt 快捷操作只把模板填入输入草稿,用户可以继续编辑;OpenCode Command 由 Runtime 原生 API 执行,输入框只承载可选参数,不以普通斜杠文本冒充执行。 @@ -618,7 +620,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。 - 支持上下文状态的 Runtime 在输入区下方复用同一紧凑用量条;文案必须区分“本次模型调用”和“压缩后对话估算”。手动压缩仅在当前 Runtime 明确支持且没有活动回复时显示,作为元信息区左下角的浮动次操作,不参与输入区高度计算;元信息区始终预留稳定高度,切换 Runtime 不得让输入框上下位移。元信息区与窗口底部只保留紧凑安全留白,不形成额外空白区。进行中禁用重复操作,结果通过应用通知反馈。 - 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。 -- 宽度大于 `700px` 时,通用工具栏内的添加内容、知识范围、专家、模式和 Runtime 选择保持同一行,Runtime 专属功能在自己的下一行横向排列。窄输入区中两行分别换行,专属选择器以至少 `220px` 的基准宽度换行而不是被挤压;不能把专属控件重新塞回通用工具栏。 +- 宽度大于 `700px` 时,主工具栏的添加内容、Runtime、模式、会话选项与发送保持同一行。窄输入区按容器宽度换行,保留发送及停止入口;选项浮层不得超出输入区宽度,不再常驻独立的 Runtime 专属功能行。 - 输入框原生支持 `Ctrl+V`:文本直接进入草稿,图片转换为本次消息附件。文件选择由上传按钮承担,不再提供独立“读取剪贴板”按钮;默认工具栏也不提供“截取当前屏幕”和“选择应用窗口”入口,避免与系统粘贴、文件选择和后续工具执行重复。 - “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。 - 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。 diff --git a/docs/features/application-tool-navigation/README.md b/docs/features/application-tool-navigation/README.md index d5777bbb..82b3f0a2 100644 --- a/docs/features/application-tool-navigation/README.md +++ b/docs/features/application-tool-navigation/README.md @@ -13,6 +13,42 @@ | [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 | | [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 | | [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 | +| [应用中心交互 Demo](./navigation-demo.html) | 对比六个候选方案;配套 CSS 和 JS 仅服务原型 | + +Demo 可直接在浏览器中打开,支持方案切换、浅深主题、添加、移除、排序和商店模拟。 +六个方案共用本次页面会话内的应用配置,刷新后恢复默认。商店、资源指标和工具内容均为 +演示数据,不执行真实安装或本机操作。窄屏上下排列侧栏与输入区,桌面展示完整应用布局。 +底部输入区共用一个简化候选布局:附件、语音、Runtime、模式和发送位于主操作行,专家、 +知识范围及 Runtime 专属选项通过“选项”展开,非默认选择显示为可移除标签。可体验文件名 +附件、模拟语音、模板填入、原生命令选择、8 秒模拟回复、发送排队、立即插入、删除、停止, +以及上下文详情和模拟压缩。文件只读取名称,语音使用固定示例文本,不录音、不上传、不调用 AI。 +这是输入区设计讨论用的原型,尚未改变正式产品或替代现有 UI 规范。 + +六个方案共用“我的应用”区:通过 + 或“全部应用”选择常驻应用,编辑时可拖动或使用 +上下箭头排序、移除入口。未常驻的已安装应用仍可从“全部应用”打开。商店包含“项目管理” +和“阅读空间”应用示例,支持仅安装或安装并常驻左侧菜单,常驻不表示后台运行。 +常驻应用在本次页面会话内跨方案保持一致;“恢复默认应用”重置安装与常驻配置。 +移除入口不会卸载应用或关闭当前页面,刷新后恢复默认。 + +魔法笔记提供应用中心“设置”、笔记页面“笔记设置”和全局设置演示入口,均打开同一设置 +面板,共用本次页面会话内的配置。面板沿用现有选项:显示未完成待办数量、AI 评论方式 +(即时/保存后自动/保存后手动)、AI 评论形式(长评 + 要点/长评/要点)。现有“显示 +魔法笔记入口”在原型中与“常驻左侧菜单”统一,不额外增加功能启停开关。侧栏待办数为 +固定的 3 项演示数据。修改即时生效于原型,不调用 AI 或写入真实应用设置。 + +| 方案 | 讨论重点 | +| --- | --- | +| A:紧凑入口 | 从底部向上展开应用列表 | +| B:应用宫格 | 在展开面板中展示应用图标、名称和常驻操作 | +| C:应用预览 | 用卡片说明应用用途 | +| D:搜索启动台 | 搜索打开能力,支持 Ctrl / Command K、方向键与 Enter | +| E:常驻管理 | 已常驻应用优先排列,可取消常驻或打开 | +| F:商店发现(未来) | 默认展开模拟商店,不纳入首期 | + +D 参考 [Raycast](https://www.raycast.com/) 的搜索启动与扩展发现方式。早期方案参考过 +Notion 收藏和 VS Code Profiles,当前版本已移除场景工具包与快捷区。 +原型支持 URL 的 `#launcher`、`#favorites`、 +`#scenes` 等片段直接打开指定方案。 ## 术语 diff --git a/docs/features/application-tool-navigation/navigation-demo.css b/docs/features/application-tool-navigation/navigation-demo.css new file mode 100644 index 00000000..1026e2cf --- /dev/null +++ b/docs/features/application-tool-navigation/navigation-demo.css @@ -0,0 +1,97 @@ +:root { + color-scheme: light; + --surface-canvas: #e8f0f5; --surface-raised: #fff; --surface-subtle: #f4f8fb; + --text-primary: #17263a; --text-secondary: #4f6278; --border-default: #cedce7; + --accent: #086fc7; --accent-subtle: #e8f4ff; --accent-selected: #a7cfea; + --font-family-ui: "Inter Variable", "PingFang SC", "Microsoft YaHei UI", "Noto Sans SC Variable", system-ui, sans-serif; + --radius-control: 10px; --radius-card: 14px; +} +[data-theme="dark"] { color-scheme: dark; --surface-canvas: #07131f; --surface-raised: #102335; --surface-subtle: #162c40; --text-primary: #edf7ff; --text-secondary: #b5c6d5; --border-default: #29445a; --accent: #72c5ff; --accent-subtle: #123b5b; --accent-selected: #28668f; } +* { box-sizing: border-box; } +body { margin: 0; padding: 24px 32px; font: 13px/1.6 var(--font-family-ui); background: var(--surface-subtle); color: var(--text-primary); } +button, input, textarea { font: inherit; color: inherit; } +button { cursor: pointer; border: 0; background: transparent; border-radius: var(--radius-control); padding: 8px 12px; } +button:hover { background: var(--accent-subtle); color: var(--accent); } +button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } +button:disabled { opacity: .45; cursor: default; } +svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; vertical-align: middle; flex-shrink: 0; } +p, h1, h2 { margin: 0; } h1 { font-size: 30px; font-weight: 550; margin: 12px 0; letter-spacing: -.8px; } h2 { font-size: 24px; } +.muted, .eyebrow { color: var(--text-secondary); }.eyebrow { font-size: 11px; letter-spacing: 1px; } +.lab-header, .design-note, .lab-footer { display: flex; align-items: center; justify-content: space-between; gap: 24px; max-width: 1440px; margin: auto; } +.lab-header strong { font-size: 16px; }.lab-header p { color: var(--text-secondary); font-size: 12px; margin-top: 4px; } +.variants { display: grid; grid-template-columns: repeat(3, 1fr); padding: 4px; border: 1px solid var(--border-default); border-radius: var(--radius-control); } +.variants button[aria-pressed="true"] { background: var(--surface-raised); color: var(--accent); box-shadow: 0 1px 3px var(--border-default); } +.design-note { margin: 24px auto 12px; font-size: 12px; }.design-note #variant-label { color: var(--accent); font-weight: 600; white-space: nowrap; }.design-note p { flex: 1; }.design-note > span:last-child { color: var(--text-secondary); font-size: 11px; } +.app { display: flex; max-width: 1440px; height: min(780px, calc(100dvh - 204px)); min-height: 590px; margin: auto; border: 1px solid var(--border-default); border-radius: var(--radius-card); overflow: hidden; background: var(--surface-raised); } +.sidebar { width: 278px; flex: 0 0 278px; background: var(--surface-canvas); display: flex; flex-direction: column; padding: 20px 12px 8px; border-right: 1px solid var(--border-default); } +.brand { display: flex; gap: 8px; align-items: center; padding: 0 8px 24px; }.brand > .muted { margin-left: auto; font-size: 11px; }.brand-mark { display: grid; place-items: center; width: 32px; height: 32px; background: var(--text-primary); color: var(--surface-raised); border-radius: 10px; font-weight: 650; font-size: 12px; } +.new-chat { border: 1px solid var(--border-default); background: var(--surface-raised); text-align: left; margin-bottom: 16px; }.new-chat span { margin-right: 8px; } +.navigation { display: grid; gap: 3px; }.navigation button, .fixed-entry button { display: flex; align-items: center; gap: 10px; text-align: left; padding: 9px 10px; }.navigation .selected { background: var(--accent-subtle); color: var(--accent); } +.history { flex: 1; min-height: 72px; overflow: auto; padding: 24px 0 8px; }.history .eyebrow { display: block; padding: 0 10px 8px; }.history button { display: block; width: 100%; text-align: left; color: var(--text-secondary); font-size: 12px; } +.fixed-entry { border-top: 1px solid var(--border-default); padding: 8px 0; }.fixed-entry button { width: 100%; } +.shortcuts { border-top: 1px solid var(--border-default); padding-top: 12px; max-height: 52%; overflow: auto; flex-shrink: 0; }.section-label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; padding-left: 8px; font-size: 11px; color: var(--text-secondary); }.section-label button { font-size: 11px; padding: 4px 8px; } +.shortcut-list { display: grid; gap: 4px; }.shortcut-item { min-width: 0; }.shortcut-button { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; }.shortcut-button.active { color: var(--accent); background: var(--accent-subtle); }.shortcut-button .icon-tile { display: grid; place-items: center; }.shortcut-button .item-name { font-size: 11px; } +[data-variant="dock"] .shortcut-list { grid-template-columns: repeat(5, minmax(0, 1fr)); background: var(--surface-raised); padding: 4px; border: 1px solid var(--border-default); border-radius: var(--radius-card); } +[data-variant="dock"] .shortcut-button { padding: 10px 0; flex-direction: column; gap: 4px; }[data-variant="dock"] .shortcut-button .item-name { display: none; }[data-variant="dock"] .shortcut-list.editing { grid-template-columns: repeat(3, minmax(0, 1fr)); }[data-variant="dock"] .editing .item-name { display: block; } +.store-link { margin-top: 8px; padding: 8px; display: flex; align-items: center; width: 100%; gap: 8px; text-align: left; font-size: 12px; }.store-link span:last-child { margin-left: auto; color: var(--text-secondary); } +[data-variant="grid"] .shortcut-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }[data-variant="grid"] .shortcut-button { flex-direction: column; gap: 4px; padding: 8px 4px; }[data-variant="grid"] .icon-tile { width: 36px; height: 36px; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); } +.featured { border: 1px solid var(--border-default); border-radius: var(--radius-control); padding: 12px; margin-top: 12px; background: var(--surface-raised); position: relative; }.featured p { font-size: 11px; color: var(--text-secondary); margin: 4px 0; }.featured .dismiss { position: absolute; right: 4px; top: 4px; padding: 0 8px; }.featured .feature-open { color: var(--accent); padding: 4px 0 0; font-size: 11px; } +[data-variant="cards"] .shortcut-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }[data-variant="cards"] .shortcut-button { background: var(--surface-raised); border: 1px solid var(--border-default); padding: 12px 8px; flex-direction: column; align-items: flex-start; }[data-variant="cards"] .shortcut-button .item-name { font-size: 12px; }.card-detail { font-size: 11px; color: var(--text-secondary); text-align: left; }.mini-meter { display: flex; gap: 3px; align-items: end; height: 24px; }.mini-meter i { display: block; width: 5px; background: var(--accent-selected); border-radius: 2px; }.mini-meter i:nth-child(3n) { background: var(--accent); } +.edit-actions { display: flex; justify-content: center; }.edit-actions button { padding: 2px 6px; font-size: 12px; }.sidebar-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-left: 8px; font-size: 11px; } +.workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; }.workspace-header { display: flex; justify-content: space-between; padding: 16px 24px; border-bottom: 1px solid var(--border-default); font-size: 12px; }.conversation { flex: 1; padding: 64px 40px 24px; max-width: 740px; width: 100%; margin: auto; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }.suggestions { display: flex; gap: 12px; margin-top: 32px; flex-wrap: wrap; }.suggestions button { border: 1px solid var(--border-default); font-size: 12px; }.suggestions span { margin-left: 24px; } +.composer-area { margin: 16px 24px 8px; flex-shrink: 0; } +.composer { padding: 12px; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-subtle); } +.composer:focus-within { border-color: var(--accent-selected); } +.composer textarea { display: block; width: 100%; height: 76px; min-height: 76px; max-height: 180px; padding: 4px; border: 0; resize: none; background: transparent; line-height: 1.8; } +.composer textarea::placeholder { color: var(--text-secondary); font-size: 12px; } +.composer-toolbar { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; margin-top: 8px; } +.composer-toolbar button { padding: 6px 8px; font-size: 12px; white-space: nowrap; } +.composer-toolbar .composer-icon { width: 32px; height: 32px; padding: 4px; font-size: 20px; } +.composer-select select { max-width: 132px; padding: 6px 2px; border: 0; background: transparent; cursor: pointer; font: inherit; font-size: 12px; color: var(--text-primary); } +select option { background: var(--surface-raised); color: var(--text-primary); } +select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } +.composer-actions { display: flex; gap: 4px; margin-left: auto; } +#send { width: 32px; height: 32px; padding: 0; font-size: 20px; background: var(--accent); color: var(--surface-raised); } +#voice[aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent); } +.composer-meta { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-height: 32px; color: var(--text-secondary); font-size: 10px; } +.composer-meta button { font-size: 10px; padding: 4px; } +#composer-tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px 8px; max-height: 96px; overflow: auto; } +#composer-tags button { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 8px; font-size: 11px; background: var(--accent-subtle); color: var(--accent); } +#composer-options, #context-details { position: fixed; inset: auto; margin: 0; width: min(320px, calc(100vw - 32px)); max-height: min(460px, 70dvh); overflow: auto; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); box-shadow: 0 8px 32px #07131f26; font-size: 12px; } +#composer-options header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } +.option-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; } +.option-row select { width: 166px; padding: 6px; border: 1px solid var(--border-default); border-radius: 6px; font: inherit; background: var(--surface-subtle); color: var(--text-primary); } +#composer-options fieldset { margin: 8px 0; padding: 12px 0; border: 0; border-top: 1px solid var(--border-default); } +#composer-options fieldset label { display: inline-flex; align-items: center; gap: 4px; margin-right: 12px; } +#composer-options input { accent-color: var(--accent); } +#composer-options p { margin-top: 12px; font-size: 11px; } +#context-details p { margin: 8px 0; color: var(--text-secondary); } +#context-details progress { width: 100%; accent-color: var(--accent); } +#compact { margin-top: 8px; color: var(--accent); background: var(--accent-subtle); } +#demo-queue { max-height: 96px; overflow: auto; margin-bottom: 8px; } +.queue-row { display: flex; align-items: center; gap: 8px; font-size: 11px; } +.queue-row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.queue-row button { flex-shrink: 0; padding: 4px; font-size: 11px; } +.workbar { width: 280px; flex-shrink: 0; border-left: 1px solid var(--border-default); background: var(--surface-subtle); }.workbar header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border-default); }#tool-content { padding: 24px 16px; }.metric { margin: 20px 0; }.metric strong { float: right; }.metric progress { width: 100%; accent-color: var(--accent); }.file-row { padding: 12px 0; border-bottom: 1px solid var(--border-default); }pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; } +.lab-footer { margin-top: 12px; font-size: 12px; color: var(--text-secondary); } +dialog { width: min(580px, calc(100% - 32px)); max-height: 85dvh; border: 1px solid var(--border-default); padding: 24px; border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); }dialog::backdrop { background: #07131f80; }dialog header { display: flex; justify-content: space-between; align-items: start; }dialog > p { margin: 12px 0 16px; }dialog input { width: 100%; padding: 10px 12px; background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-control); }.catalog-row { display: flex; align-items: center; gap: 12px; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.catalog-row > div { flex: 1; }.catalog-row p { font-size: 11px; color: var(--text-secondary); }.catalog-row button { color: var(--accent); background: var(--accent-subtle); font-size: 12px; }dialog footer { font-size: 11px; color: var(--text-secondary); margin-top: 16px; } +#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); padding: 10px 20px; border-radius: var(--radius-control); background: var(--text-primary); color: var(--surface-raised); pointer-events: none; opacity: 0; }#toast.visible { opacity: 1; } +[hidden] { display: none !important; } +#page-actions { margin-left: auto; }#page-actions button { padding: 4px 8px; font-size: 12px; }.note-count { margin-left: auto; padding: 0 6px; background: var(--accent-subtle); border-radius: var(--radius-control); color: var(--accent); }.note-setting-row { display: flex; gap: 16px; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.note-setting-row p, .note-setting-options p { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }.demo-switch { width: 36px; height: 20px; border-radius: 20px; border: 1px solid var(--text-secondary); background: var(--surface-canvas); padding: 2px; flex-shrink: 0; }.demo-switch::after { content: ''; display: block; width: 14px; height: 14px; border-radius: 50%; background: var(--text-secondary); }.demo-switch[aria-checked="true"] { background: var(--accent); border-color: var(--accent); }.demo-switch[aria-checked="true"]::after { margin-left: 16px; background: var(--surface-raised); }.note-setting-options { border: 0; padding: 16px 0; margin: 0; min-width: 0; }.note-setting-options legend { padding-top: 16px; font-weight: 550; }.note-segments { display: flex; padding: 4px; gap: 4px; background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-control); }.note-segments button { flex: 1; padding: 8px 4px; font-size: 12px; }.note-segments [aria-pressed="true"] { color: var(--accent); background: var(--surface-raised); box-shadow: 0 1px 3px var(--border-default); }.note-settings-preview { padding: 16px; background: var(--accent-subtle); border-radius: var(--radius-control); }.note-settings-preview small { color: var(--text-secondary); font-size: 11px; }#back-to-apps { margin-bottom: 12px; padding-left: 0; color: var(--accent); }#note-settings-summary { margin: 16px 0; } +.sidebar-footer { border-top: 1px solid var(--border-default); padding: 8px 0 0; gap: 4px; }#center-entry { flex: 1; min-width: 0; }#open-center { width: 100%; display: flex; align-items: center; gap: 10px; padding: 8px; text-align: left; }#open-center strong { font-size: 13px; font-weight: 550; }#open-center small { display: block; font-size: 11px; color: var(--text-secondary); }.entry-arrow { margin-left: auto; }.tool-tabs { display: flex; padding: 8px; gap: 2px; }.tool-tabs button { flex: 1; padding: 8px 4px; font-size: 12px; }.tool-tabs [aria-selected="true"] { background: var(--accent-subtle); color: var(--accent); }.center-switch { display: flex; gap: 4px; padding-bottom: 12px; }.center-switch [aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent); }.center-app { display: flex; gap: 12px; align-items: start; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.center-app > svg { margin-top: 4px; color: var(--accent); }.center-app-info { flex: 1; }.center-app p { color: var(--text-secondary); font-size: 11px; }.center-actions { display: flex; gap: 4px; flex-wrap: wrap; }.center-actions button { font-size: 11px; background: var(--accent-subtle); color: var(--accent); }.center-actions [aria-pressed="true"] { border: 1px solid var(--accent-selected); } +dialog[data-variant="dock"], dialog[data-variant="grid"], dialog[data-variant="favorites"] { position: fixed; left: 48px; bottom: 100px; top: auto; right: auto; margin: 0; width: min(520px, calc(100% - 32px)); max-height: 72dvh; } +dialog[data-variant="grid"] #catalog-items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }dialog[data-variant="grid"] .center-app { flex-direction: column; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }dialog[data-variant="cards"] .center-app { padding: 24px 0; }dialog[data-variant="cards"] .center-app > svg { width: 32px; height: 32px; }dialog[data-variant="cards"] .app-detail { margin-top: 8px; font-size: 13px; }dialog[data-variant="favorites"] .app-detail { display: none; }dialog[data-variant="scenes"] .center-app { background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-card); padding: 20px; margin-top: 16px; flex-wrap: wrap; }dialog[data-variant="scenes"] .center-actions { width: 100%; justify-content: flex-end; } +@media (max-width: 640px) { dialog[data-variant] { left: 16px; right: 16px; bottom: 16px; top: auto; margin: 0; max-height: 85dvh; width: calc(100% - 32px); padding: 16px; }.center-app { flex-wrap: wrap; }.center-actions { width: 100%; }.app { flex-direction: column; }.app .sidebar { flex: 1; min-height: 0; }.app .workspace { display: block; flex: 0 0 auto; }.app .workspace .conversation, .app .workspace .composer { display: none; }.app .workspace-header { padding: 8px 16px; align-items: center; }.app .workbar { z-index: 2; } } +.reference-note { max-width: 1440px; margin: 12px auto 0; display: flex; flex-wrap: wrap; gap: 8px 16px; color: var(--text-secondary); font-size: 11px; }.reference-note a { color: var(--accent); text-underline-offset: 3px; } +.sidebar > * { flex-shrink: 0; }.sidebar .history { flex: 1 1 80px; min-height: 0; }.sidebar .shortcuts { flex-shrink: 1; min-height: 80px; } +.sidebar .resident-apps { margin-top: 12px; min-height: 96px; flex-shrink: 1; display: flex; flex-direction: column; max-height: 240px; }.resident-apps .section-label { margin-bottom: 4px; flex-shrink: 0; }#resident-list { overflow: auto; min-height: 0; }.resident-row { display: flex; align-items: center; }.resident-row > button { display: flex; align-items: center; gap: 10px; padding: 8px 10px; flex: 1; min-width: 0; text-align: left; }.resident-row > button span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.resident-row [aria-current="page"] { background: var(--accent-subtle); color: var(--accent); }.resident-row[draggable="true"] { cursor: grab; }.resident-row .edit-actions { flex-shrink: 0; }.all-apps { display: flex; justify-content: space-between; padding: 6px 10px; font-size: 11px; color: var(--text-secondary); flex-shrink: 0; }.empty-apps { padding: 8px 10px; font-size: 11px; color: var(--text-secondary); }.app-placement { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }.app-placement button { font-size: 11px; padding: 6px 8px; }.app-placement button[aria-pressed="true"] { border: 1px solid var(--accent-selected); }.app-catalog-row { align-items: flex-start; }.app-catalog-row > svg { margin-top: 4px; } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } +.launcher-trigger { display: flex; align-items: center; gap: 8px; width: 100%; padding: 12px 8px; border: 1px solid var(--border-default); background: var(--surface-raised); margin-bottom: 12px; font-size: 12px; }.launcher-trigger kbd { margin-left: auto; font: 10px var(--font-family-ui); color: var(--text-secondary); white-space: nowrap; } +[data-variant="launcher"] .shortcut-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }[data-variant="launcher"] .shortcut-button { flex-direction: column; padding: 8px 0; }[data-variant="launcher"] .icon-tile { height: 24px; }[data-variant="launcher"] .store-link { border-top: 1px solid var(--border-default); border-radius: 0; } +[data-variant="favorites"] .section-label { padding-left: 0; }[data-variant="favorites"] .shortcut-list { gap: 0; }[data-variant="favorites"] .shortcut-button { justify-content: flex-start; padding: 8px 12px; }[data-variant="favorites"] .item-name { font-size: 12px; }[data-variant="favorites"] .shortcut-item { display: flex; align-items: center; }[data-variant="favorites"] .edit-actions { margin-left: auto; }[data-variant="favorites"] .store-link { margin-top: 12px; border: 1px dashed var(--border-default); }.count { padding: 0 6px; color: var(--text-secondary); } +.scene-banner { display: flex; align-items: center; gap: 12px; margin: 0 4px 12px; padding: 12px; background: var(--accent-subtle); border-radius: var(--radius-control); }.scene-banner > svg { color: var(--accent); width: 24px; height: 24px; }.scene-banner .eyebrow { letter-spacing: 0; font-size: 10px; }.scene-count { margin-left: auto; font-size: 11px; color: var(--text-secondary); }.scene-label { display: block; }.scene-label select { font: inherit; font-weight: 600; color: var(--text-primary); background: transparent; border: 0; padding: 0; cursor: pointer; }.scene-label option { background: var(--surface-raised); } +[data-variant="scenes"] .shortcut-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }[data-variant="scenes"] .shortcut-button { border: 1px solid var(--border-default); background: var(--surface-raised); padding: 10px 8px; justify-content: flex-start; }.pack-preview { display: flex; gap: 8px; align-items: center; width: 100%; margin-top: 12px; padding: 12px 8px; text-align: left; background: var(--surface-raised); border: 1px solid var(--border-default); }.pack-preview strong { font-size: 11px; font-weight: 550; }.pack-preview small { display: block; color: var(--text-secondary); font-size: 10px; }.pack-preview > span:last-child { margin-left: auto; }.pack-symbol { color: var(--accent); } +.catalog-pack { padding: 20px 0; border-bottom: 1px solid var(--border-default); }.catalog-pack h3 { margin: 8px 0; font-size: 18px; font-weight: 550; }.pack-art { display: grid; place-items: center; float: right; width: 56px; height: 56px; border: 1px solid var(--accent-selected); background: var(--accent-subtle); color: var(--accent); border-radius: var(--radius-card); }.pack-art svg { width: 28px; height: 28px; }.pack-includes { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }.pack-includes span { padding: 4px 8px; font-size: 11px; background: var(--surface-subtle); border-radius: var(--radius-control); }.pack-includes svg { width: 14px; height: 14px; }.catalog-pack > button { border: 1px solid var(--accent-selected); color: var(--accent); background: var(--accent-subtle); } +dialog[data-mode="launch"] { width: min(640px, calc(100% - 32px)); }dialog[data-mode="launch"] .catalog-row:focus-within { background: var(--accent-subtle); }.catalog-row { padding-left: 8px; padding-right: 8px; } +@media (max-width: 1080px) { body { padding: 16px; }.app { position: relative; }.workbar { position: absolute; right: 0; top: 52px; bottom: 0; width: min(320px, 100%); box-shadow: -8px 0 24px var(--border-default); }.lab-header { flex-wrap: wrap; gap: 12px; }.design-note > span:last-child { display: none; }.conversation { padding: 32px 24px; } } +@media (max-width: 640px) { body { padding: 12px; }.lab-header > div:first-child { flex: 1; }.variants { order: 3; width: 100%; }.variants button { flex: 1; padding: 8px 4px; font-size: 12px; }.design-note { align-items: flex-start; gap: 12px; }.app { height: auto; min-height: 0; }.app .sidebar { width: 100%; flex: 0 0 auto; border: 0; padding-top: 12px; }.brand { padding-bottom: 12px; }.app .history { display: none; }.app .workspace { display: flex; min-height: 320px; border-top: 1px solid var(--border-default); }.app .workspace .composer { display: block; }.composer-area { margin: 12px 12px 4px; margin-top: auto; }.composer-toolbar { gap: 2px; }.composer-select select { max-width: 108px; font-size: 11px; }.composer-toolbar button { font-size: 11px; padding: 6px; }.composer-toolbar .composer-icon { width: 28px; }.composer-meta { flex-wrap: wrap; gap: 0; }.shortcut-list { max-height: 300px; overflow: auto; }.lab-footer { align-items: flex-start; }.lab-footer button { white-space: nowrap; }.workbar { top: 0; width: 100%; }.design-note p { font-size: 11px; } } diff --git a/docs/features/application-tool-navigation/navigation-demo.html b/docs/features/application-tool-navigation/navigation-demo.html new file mode 100644 index 00000000..36e90b11 --- /dev/null +++ b/docs/features/application-tool-navigation/navigation-demo.html @@ -0,0 +1,78 @@ + + +
+ + +左下角应用中心 · 六个交互方案
对话留在这里,常用能力随手打开。
+ +非默认选项会显示在输入框上方。
+本次模型调用:84k / 200k tokens
对应“显示魔法笔记入口”,移除后仍可从应用中心打开。
在左侧入口显示数量,超过 99 项显示 99+。本 Demo 使用 3 项示例。
${noteModes[noteSettings.mode]} · ${noteFormats[noteSettings.format]}
选择后即时保留于本次 Demo 会话,不调用 AI、不修改真实设置。点击 +,选择常驻应用。
'; + const subtitles = { dock: '', grid: `${installed.size} 个已安装应用`, cards: '笔记 · 知识 · 自动化', launcher: 'Ctrl / ⌘ K', favorites: `${residentApps.length} 个应用常驻菜单`, scenes: '发现你的下一个好应用' }; + $('#center-entry').innerHTML = ``; +} +function renderCatalog() { + const query = $('#search').value.trim().toLowerCase(); + const matches = items.filter(item => (catalogMode === 'store' ? !item.builtin : installed.has(item.id)) && `${item.name}${item.detail}`.toLowerCase().includes(query)); + if (variant === 'favorites') matches.sort((a, b) => Number(residentApps.includes(b.id)) - Number(residentApps.includes(a.id))); + $('#catalog-items').innerHTML = matches.map(item => { + const ready = installed.has(item.id); const pinned = residentApps.includes(item.id); + return `${item.builtin ? '内置应用' : '商店应用 · 示例'}
${variant === 'cards' ? item.summary : item.detail}
没有匹配的应用。可以换个关键词,或切换到商店。
'; + document.querySelectorAll('.center-switch button').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.catalog === catalogMode))); +} +function openCatalog(mode = 'apps') { + settingsOpen = false; $('#back-to-apps').hidden = true; $('#app-settings').hidden = true; + for (const selector of ['.center-switch', '#search', '#catalog-items']) $(selector).hidden = false; + catalogMode = mode; $('#catalog').dataset.variant = variant; + $('#catalog-title').textContent = mode === 'store' ? '发现新应用' : '应用中心'; + $('#catalog-description').textContent = mode === 'store' ? '安装你需要的应用,自主决定是否常驻左侧菜单。' : '打开应用,或把常用应用留在左侧。系统工具请使用右侧工作栏。'; + $('#search').value = ''; renderCatalog(); + if (!$('#catalog').open) $('#catalog').showModal(); + $('#search').focus(); +} +function openPage(name) { + if ($('#catalog').open) $('#catalog').close(); + $('#page-label').textContent = name; + $('#page-content').replaceChildren(); + const title = document.createElement('h1'); title.textContent = name; + const description = document.createElement('p'); description.className = 'muted'; description.textContent = '主内容区页面预览。右侧工作栏保持当前工具状态。'; + $('#page-content').append(title, description); + $('#page-actions').innerHTML = name === '魔法笔记' ? `` : ''; + if (name === '魔法笔记' || name === '设置') { + const summary = document.createElement('p'); summary.id = 'note-settings-summary'; summary.className = 'muted'; $('#page-content').append(summary); + if (name === '设置') { const button = document.createElement('button'); button.dataset.noteSettings = ''; button.textContent = '魔法笔记设置'; $('#page-content').append(button); } + renderNoteSettings(); + } + document.querySelectorAll('.navigation button').forEach(button => button.classList.toggle('selected', button.dataset.page === name)); + renderApps(); notify(`已打开${name}`); +} +function selectVariant(name) { + variant = name; $('.app').dataset.variant = name; + document.querySelectorAll('button[data-variant]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.variant === name))); + [$('#variant-label').textContent, $('#variant-description').textContent, $('#design-tradeoff').textContent] = variants[name]; + history.replaceState(null, '', `#${name}`); renderApps(); +} +function selectTool(id) { + const content = { + files: '本机 / goodbuddy · 示例PS > git status\nOn branch main\nworking tree clean\n\nPS >', + browser: '浏览器 · 静态示例
磁盘 IO:读 12 MB/s · 写 3 MB/s
' + }; + $('#tool-content').innerHTML = content[id]; $('#tool-content').setAttribute('aria-labelledby', `tab-${id}`); + document.querySelectorAll('[data-tool]').forEach(button => { button.setAttribute('aria-selected', String(button.dataset.tool === id)); button.tabIndex = button.dataset.tool === id ? 0 : -1; }); +} +document.addEventListener('click', event => { + const button = event.target.closest('button'); if (!button) return; + const data = button.dataset; + if (data.variant) selectVariant(data.variant); + if (data.catalog) openCatalog(data.catalog); + if (data.page) openPage(data.page); + if (data.open) openPage(items.find(item => item.id === data.open).name); + if (data.tool) selectTool(data.tool); + if (button.hasAttribute('data-note-settings')) showNoteSettings(); + if (data.noteToggle || data.noteMode || data.noteFormat) { + if (data.noteToggle === 'resident') residentApps = residentApps.includes('notes') ? residentApps.filter(id => id !== 'notes') : [...residentApps, 'notes']; + if (data.noteToggle === 'showCount') noteSettings.showCount = !noteSettings.showCount; + if (data.noteMode) noteSettings.mode = data.noteMode; + if (data.noteFormat) noteSettings.format = data.noteFormat; + renderApps(); renderNoteSettings(); + const selector = data.noteToggle ? `[data-note-toggle="${data.noteToggle}"]` : data.noteMode ? `[data-note-mode="${data.noteMode}"]` : `[data-note-format="${data.noteFormat}"]`; + $(selector).focus(); + } + if (button.id === 'open-center') openCatalog(variant === 'scenes' ? 'store' : 'apps'); + if (button.id === 'edit-apps') { editingApps = !editingApps; renderApps(); $('#edit-apps').focus(); } + if (data.pin || data.install || data.installPin) { + const id = data.pin || data.install || data.installPin; + if (data.install || data.installPin) installed.add(id); + if (data.pin || data.installPin) residentApps = residentApps.includes(id) ? residentApps.filter(entry => entry !== id) : [...residentApps, id]; + renderApps(); + if ($('#catalog').open) { renderCatalog(); ($(`#catalog [data-pin="${id}"]`) || $('#search')).focus(); } + else $('#edit-apps').focus(); + } + if (data.move) { + const from = residentApps.indexOf(data.move); const to = from + Number(data.direction); + if (to >= 0 && to < residentApps.length) [residentApps[from], residentApps[to]] = [residentApps[to], residentApps[from]]; + renderApps(); $(`#resident-list [data-pin="${data.move}"]`).focus(); + } +}); +$('#resident-list').addEventListener('dragstart', event => { draggedApp = event.target.closest('[data-resident]')?.dataset.resident || ''; if (!editingApps || !draggedApp) { event.preventDefault(); return; } event.dataTransfer.setData('text/plain', draggedApp); }); +$('#resident-list').addEventListener('dragover', event => { if (editingApps && draggedApp) event.preventDefault(); }); +$('#resident-list').addEventListener('drop', event => { + event.preventDefault(); const target = event.target.closest('[data-resident]')?.dataset.resident; + if (!editingApps || !residentApps.includes(draggedApp) || !target || target === draggedApp) return; + const to = residentApps.indexOf(target); residentApps.splice(residentApps.indexOf(draggedApp), 1); residentApps.splice(to, 0, draggedApp); draggedApp = ''; renderApps(); $('#edit-apps').focus(); +}); +$('#resident-list').addEventListener('dragend', () => { draggedApp = ''; }); +$('#search').addEventListener('input', renderCatalog); +$('#close-catalog').onclick = () => $('#catalog').close(); +$('#back-to-apps').onclick = () => { const query = $('#search').value; openCatalog(catalogMode); $('#search').value = query; renderCatalog(); ($('#catalog [data-note-settings]') || $('#search')).focus(); }; +function toggleWorkbar(visible) { $('.workbar').hidden = !visible; $('#toggle-workbar').setAttribute('aria-expanded', String(visible)); } +$('#toggle-workbar').onclick = () => toggleWorkbar($('.workbar').hidden); +$('#close-tool').onclick = () => { toggleWorkbar(false); $('#toggle-workbar').focus(); }; +$('.tool-tabs').addEventListener('keydown', event => { + const tabs = [...document.querySelectorAll('[data-tool]')]; const index = tabs.indexOf(event.target); + if (index < 0 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length; + selectTool(tabs[next].dataset.tool); tabs[next].focus(); +}); +document.addEventListener('keydown', event => { + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); openCatalog(); } + if (!$('#catalog').open || settingsOpen || variant !== 'launcher') return; + const actions = [...document.querySelectorAll('#catalog [data-open]')]; if (!actions.length) return; + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); const index = actions.indexOf(document.activeElement); const next = index < 0 ? (event.key === 'ArrowDown' ? 0 : actions.length - 1) : (index + (event.key === 'ArrowDown' ? 1 : -1) + actions.length) % actions.length; actions[next].focus(); } + if (event.key === 'Enter' && document.activeElement === $('#search')) { event.preventDefault(); actions[0].click(); } +}); +$('#theme').onclick = () => { const dark = document.documentElement.dataset.theme !== 'dark'; document.documentElement.dataset.theme = dark ? 'dark' : 'light'; $('#theme').textContent = dark ? '切换浅色' : '切换深色'; }; +$('#reset').onclick = () => { installed = new Set(items.filter(item => item.builtin).map(item => item.id)); residentApps = ['notes', 'knowledge']; editingApps = false; renderApps(); notify('应用安装与常驻配置已恢复默认'); }; +let demoRunning = false; +let demoRecording = false; +let demoAttachments = []; +let demoQueue = []; +let demoReplyTimer; +function renderComposer() { + const tags = $('#composer-tags'); tags.replaceChildren(); + const addTag = (text, clear) => { + const button = document.createElement('button'); button.textContent = `${text} ×`; button.setAttribute('aria-label', `移除${text}`); + button.onclick = () => { clear(); renderComposer(); $('#message').focus(); }; tags.append(button); + }; + demoAttachments.forEach((name, index) => addTag(name, () => demoAttachments.splice(index, 1))); + for (const id of ['composer-expert', 'composer-agent', 'composer-command']) { + const select = $(`#${id}`); + if (select.value) addTag(select.selectedOptions[0].textContent, () => { select.value = ''; }); + } + document.querySelectorAll('[data-knowledge]:checked').forEach(input => addTag(input.dataset.knowledge, () => { input.checked = false; })); + if ($('#composer-retrieval').value === 'always') addTag('每次检索', () => { $('#composer-retrieval').value = 'auto'; }); + tags.hidden = !tags.childElementCount; + $('#stop').hidden = !demoRunning; + $('#send').disabled = !$('#message').value.trim() && $('#composer-command').value !== 'command'; + $('#send').title = demoRunning ? '加入队列' : '发送'; + $('#send').setAttribute('aria-label', demoRunning ? '加入演示队列' : '发送演示消息'); + $('#composer-status').textContent = demoRecording ? '模拟录音中 · 再次点击结束' : demoRunning ? '模拟回复中 · 可继续发送加入队列' : '交互演示 · 不调用 AI'; + for (const id of ['composer-runtime', 'composer-mode', 'composer-expert', 'composer-agent', 'composer-command']) $(`#${id}`).disabled = demoRunning; + $('#compact').disabled = demoRunning; + $('#runtime-options').hidden = $('#composer-runtime').value === '直连模型'; + $('#compact').hidden = $('#composer-runtime').value === '直连模型'; + $('#demo-queue').replaceChildren(); + demoQueue.forEach((text, index) => { + const row = document.createElement('div'); row.className = 'queue-row'; + const label = document.createElement('span'); label.textContent = text; label.title = text; + const insert = document.createElement('button'); insert.textContent = '立即插入'; insert.setAttribute('aria-label', `立即中断并插入:${text}`); + insert.onclick = () => { demoQueue.splice(index, 1); startDemoReply(text); }; + const remove = document.createElement('button'); remove.textContent = '×'; remove.setAttribute('aria-label', `删除待发送消息:${text}`); + remove.onclick = () => { demoQueue.splice(index, 1); renderComposer(); }; + row.append(label, insert, remove); $('#demo-queue').append(row); + }); + $('#demo-queue').hidden = !demoQueue.length; +} +function startDemoReply(text) { + clearTimeout(demoReplyTimer); demoRunning = true; + if ($('#composer-options').matches(':popover-open')) $('#composer-options').hidePopover(); + notify(`模拟处理:${text}`); renderComposer(); + demoReplyTimer = setTimeout(() => { + demoRunning = false; + if (demoQueue.length) startDemoReply(demoQueue.shift()); + else { renderComposer(); notify('演示回复已完成,未调用 AI'); } + }, 8000); +} +$('#send').onclick = () => { + if ($('#send').disabled) return; + const text = $('#composer-command').value === 'command' ? `/init ${$('#message').value.trim()}` : $('#message').value.trim(); + $('#message').value = ''; $('#message').style.height = ''; demoAttachments = []; + if (demoRunning) { demoQueue.push(text); renderComposer(); } + else startDemoReply(text); +}; +$('#stop').onclick = () => { + clearTimeout(demoReplyTimer); demoRunning = false; + if (demoQueue.length) startDemoReply(demoQueue.shift()); + else { renderComposer(); notify('已停止演示回复'); } +}; +$('#message').addEventListener('input', () => { $('#message').style.height = 'auto'; $('#message').style.height = `${Math.min(180, $('#message').scrollHeight)}px`; renderComposer(); }); +$('#message').addEventListener('keydown', event => { if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); $('#send').click(); } }); +$('#attach').onclick = () => $('#attachment-input').click(); +$('#attachment-input').onchange = () => { demoAttachments.push(...Array.from($('#attachment-input').files, file => file.name)); $('#attachment-input').value = ''; renderComposer(); }; +$('#voice').onclick = () => { + demoRecording = !demoRecording; $('#voice').setAttribute('aria-pressed', String(demoRecording)); + if (!demoRecording) { $('#message').value += '请帮我整理一下今天的工作计划。'; $('#message').dispatchEvent(new Event('input')); } + renderComposer(); +}; +$('#composer-options').addEventListener('change', event => { + if (event.target.id === 'composer-command' && event.target.value === 'prompt') { + $('#message').value = '请总结当前讨论,列出关键结论和下一步行动。'; event.target.value = ''; $('#message').dispatchEvent(new Event('input')); + } + renderComposer(); +}); +$('#composer-runtime').onchange = () => { + $('#composer-agent').value = ''; $('#composer-command').value = ''; + $('#agent-label').textContent = $('#composer-runtime').value === 'Continue' ? '预设' : 'Agent'; + $('#composer-command').querySelector('[value="command"]').hidden = $('#composer-runtime').value !== 'OpenCode'; + renderComposer(); +}; +$('#compact').onclick = () => { + $('#context-value').textContent = '18%'; $('#context-progress').value = 18; + $('#context-description').textContent = '压缩后对话估算:36k / 200k tokens'; + $('#context-details').hidePopover(); notify('已模拟压缩上下文'); +}; +for (const [panelId, triggerId] of [['composer-options', 'composer-options-trigger'], ['context-details', 'context-trigger']]) { + const panel = $(`#${panelId}`); const trigger = $(`#${triggerId}`); + const position = () => { + const rect = trigger.getBoundingClientRect(); + panel.style.left = `${Math.max(16, Math.min(rect.right - panel.offsetWidth, innerWidth - panel.offsetWidth - 16))}px`; + panel.style.top = `${Math.max(16, rect.top - panel.offsetHeight - 8)}px`; + }; + panel.addEventListener('toggle', event => { trigger.setAttribute('aria-expanded', String(event.newState === 'open')); if (event.newState === 'open') position(); }); + window.addEventListener('resize', () => { if (panel.matches(':popover-open')) position(); }); + window.addEventListener('scroll', () => { if (panel.matches(':popover-open')) position(); }, true); +} +renderComposer(); +document.querySelectorAll('[data-icon]').forEach(element => { element.innerHTML = icon(element.dataset.icon); }); +window.addEventListener('hashchange', () => { if (Object.hasOwn(variants, location.hash.slice(1))) selectVariant(location.hash.slice(1)); }); +selectVariant(Object.hasOwn(variants, location.hash.slice(1)) ? location.hash.slice(1) : 'dock'); selectTool('files'); +if (matchMedia('(max-width: 1080px)').matches) toggleWorkbar(false); diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index c8f796fd..ada841cb 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -940,11 +940,17 @@ const api: DesktopApi = { function composerMenuTrigger( label: "专家角色" | "工作模式", ): HTMLButtonElement { + if (label === "专家角色") openComposerOptions(); return screen.getByRole("button", { name: new RegExp(`^${label}:`, "u"), }); } +function openComposerOptions(): void { + const trigger = screen.getByRole("button", { name: "选项" }); + if (trigger.getAttribute("aria-expanded") !== "true") fireEvent.click(trigger); +} + function openComposerMenu(label: "专家角色" | "工作模式"): HTMLElement { fireEvent.click(composerMenuTrigger(label)); return screen.getByRole("menu", { name: label }); @@ -4726,6 +4732,7 @@ describe("App", () => { evidence: [], }); render(