Просмотр исходного кода

优化会话侧栏与任务入口并升级至 v0.4.10

Samien 1 месяц назад
Родитель
Сommit
b1c6155951

+ 4 - 4
docs/verification/2026-09-10-bot-panels/README.md

@@ -3,11 +3,11 @@
 ## 完成内容
 
 - 共享本地虚拟机按同一账号、同一执行节点绑定多个机器人,不再按基础 Profile 分成多台。虚拟机运行数量上限单独管理;独立模式仍保留。共享电脑保留已确定的工作目录,机器人保留独立会话。
-- 机器人侧栏包含电脑、定时任务和 Inspector。电脑选择参照 OpenMausBot:自动、Grok Bot 云端、本地虚拟机、本机、仅浏览器、不使用电脑。当前执行节点没有本机控制与独立浏览器连接,因此对应两项不可选。
+- 机器人侧栏将电脑与定时任务作为居中的同组入口;Inspector 独立放在会话标题栏的聊天设置之后,标题栏与面板标题统一使用原侧栏的小虫图标。电脑选择参照 OpenMausBot:自动、Grok Bot 云端、本地虚拟机、本机、仅浏览器、不使用电脑。当前执行节点没有本机控制与独立浏览器连接,因此对应两项不可选。
 - 自动选择保留可用本地虚拟机;新机器人可使用已就绪的本地执行节点,否则使用当前账号已连接的 Grok Bot 云端。查看侧栏只查询状态;显式打开云端电脑或实际工具执行才建立/唤醒连接。
 - Grok Bot 使用服务端保存的加密凭据,按账号复用同一台虚拟机。机器人通过当前基础 Hermes 的工具桥获得云端 shell、文件读写、截图与桌面输入工具;云端地址和认证令牌不下发到浏览器或手机。
 - 接管等待当前云端电脑操作完成,后续机器人电脑工具等待交还。两个页面不能同时取得控制;控制到期不自行恢复,重新接管并交还后才继续,交还说明进入后续工具结果。
-- 群聊侧栏只有 Inspector,没有电脑查看或控制入口。
+- 群聊没有电脑查看或控制入口;任务选择器使用胶囊外观,并可从下拉末项直接新建、切换任务。聊天列表操作菜单可按类型“查看机器人”或“查看群聊”。
 - 编辑机器人允许切换基础机器人,服务端校验来源授权、可用性和空闲状态。新执行使用新会话,近期聊天记录仍用于上下文。
 - 定时任务支持单次、间隔、每日、每周和明确时区。任务、下次执行时间与执行记录持久化,重启后补执行一次到期任务,不集中重放漏掉的所有周期;相同任务未结束时跳过重叠执行。结果进入机器人原聊天。
 - 定时任务排版对齐 OpenMausBot 的 RoutinesSection / RoutineList / RoutineLogs:顶部操作卡、独立任务卡、状态标记、下一次执行、最近结果、日志筛选与模态编辑器。仅编辑标题或提示词不会推迟原定执行时间。
@@ -18,9 +18,9 @@
 ## 验证
 
 - 使用独立测试目录验证 Grok Bot 0.47.0 连接:状态读取成功,真实 shell 返回 Linux x86_64、box、/workspace,退出码 0;读取到 1280×800 PNG 桌面截图。未向真实云端发送桌面输入或改动工作文件。
-- Web Playwright 使用一次性 Hermes fixture,覆盖编辑基础机器人、真实网关请求记录、定时任务创建/立即执行/完成、群聊移除电脑入口,以及 375px 深色/减少动态效果显示。这里验证应用链路,fixture 回复不作为真实模型推理证据。
+- Web Playwright 使用一次性 Hermes fixture,覆盖编辑基础机器人、真实网关请求记录、独立 Inspector、居中的电脑/定时任务入口、定时任务创建/立即执行/完成、群聊任务新建与切换、群聊移除电脑入口,以及 375px 深色/减少动态效果显示。这里验证应用链路,fixture 回复不作为真实模型推理证据。
 - 全量 Vitest:871 项通过、7 项按原配置跳过;生产构建通过。Web 完整侧栏流程 Playwright:1 项通过。iOS 本地签名模拟器:WorkspaceChatTests 33 项、界面测试 3 项通过。
-- `desktop-computer.png`、`desktop-inspector.png`、`desktop-routines.png`、`phone-group-inspector.png` 均为本次 fixture 的实际浏览器截图。
+- `desktop-computer.png`、`desktop-inspector.png`、`desktop-group-task-picker.png`、`desktop-routines.png`、`phone-group-inspector.png` 均为本次 fixture 的实际浏览器截图。
 
 ## 启用
 

BIN
docs/verification/2026-09-10-bot-panels/desktop-computer.png


BIN
docs/verification/2026-09-10-bot-panels/desktop-group-task-picker.png


BIN
docs/verification/2026-09-10-bot-panels/desktop-inspector.png


BIN
docs/verification/2026-09-10-bot-panels/desktop-routine-logs.png


BIN
docs/verification/2026-09-10-bot-panels/desktop-routines.png


BIN
docs/verification/2026-09-10-bot-panels/phone-group-inspector.png


+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "yaoyao",
-  "version": "0.4.9",
+  "version": "0.4.10",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "yaoyao",
-      "version": "0.4.9",
+      "version": "0.4.10",
       "license": "Apache-2.0",
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yaoyao",
-  "version": "0.4.9",
+  "version": "0.4.10",
   "private": true,
   "license": "Apache-2.0",
   "description": "夭夭 AI:聊天、多 Agent 团队、文件库与电脑工作空间",

+ 3 - 3
release.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.4.9",
-  "webVersion": "0.4.9",
-  "gitTag": "v0.4.9"
+  "releaseVersion": "0.4.10",
+  "webVersion": "0.4.10",
+  "gitTag": "v0.4.10"
 }

+ 3 - 1
src/client/components/workspace/ConversationList.vue

@@ -14,7 +14,7 @@ const rows = computed(() => props.conversations.filter(c => !c.archived).map(c =
 })))
 function openMenu(id: string, event: MouseEvent) {
   menuId.value = id
-  menuPosition.value = { left: `${Math.max(8, Math.min(event.clientX, window.innerWidth - 170))}px`, top: `${Math.max(8, Math.min(event.clientY, window.innerHeight - 140))}px` }
+  menuPosition.value = { left: `${Math.max(8, Math.min(event.clientX, window.innerWidth - 170))}px`, top: `${Math.max(8, Math.min(event.clientY, window.innerHeight - 180))}px` }
 }
 function action(kind: 'pin' | 'archive' | 'delete') {
   if (kind === 'pin') emit('pin', menuId.value)
@@ -22,6 +22,7 @@ function action(kind: 'pin' | 'archive' | 'delete') {
   else emit('delete', menuId.value)
   menuId.value = ''
 }
+function viewConversation(){emit('select',menuId.value);menuId.value=''}
 </script>
 <template>
   <div class="conversation-list">
@@ -31,6 +32,7 @@ function action(kind: 'pin' | 'archive' | 'delete') {
     <Teleport to="body">
       <div v-if="menuConversation" class="conversation-menu-dismiss" @pointerdown.self="menuId = ''" @keydown.esc="menuId = ''">
         <section class="conversation-actions" :style="menuPosition" role="menu" aria-label="聊天操作">
+          <button role="menuitem" @click="viewConversation">{{ menuConversation.kind === 'direct' ? '查看机器人' : '查看群聊' }}</button>
           <button role="menuitem" @click="action('pin')">{{ menuConversation.pinned ? '取消置顶' : '置顶聊天' }}</button>
           <button role="menuitem" @click="action('archive')">{{ menuConversation.archived ? '恢复聊天' : '归档聊天' }}</button>
           <button class="danger" role="menuitem" @click="action('delete')">删除聊天</button>

+ 8 - 8
src/client/components/workspace/WorkspaceBotPanel.vue

@@ -5,20 +5,20 @@ import LocalVmChatPanel from './LocalVmChatPanel.vue'
 import WorkspaceInspectorPanel from './WorkspaceInspectorPanel.vue'
 import WorkspaceRoutinesPanel from './WorkspaceRoutinesPanel.vue'
 import type {WorkspaceAgent} from '@shared/workspace'
-const props=defineProps<{agents:WorkspaceAgent[];conversationId:string;taskId?:string;group:boolean;isAdmin:boolean;active?:boolean;initialView?:'computer'|'routines'|'inspector'}>()
+const props=defineProps<{agents:WorkspaceAgent[];conversationId:string;taskId?:string;mode:'computer'|'inspector';isAdmin:boolean;active?:boolean}>()
 const emit=defineEmits<{close:[];changed:[];settings:[];desktop:[agent:WorkspaceAgent];workspace:[agent:WorkspaceAgent]}>()
-const view=ref<'computer'|'routines'|'inspector'>('computer')
-watch(()=>[props.conversationId,props.initialView],()=>{let saved;try{saved=localStorage.getItem('yaoyao-bot-panel:'+props.conversationId)}catch{}view.value=props.group?'inspector':props.initialView??(['computer','routines','inspector'].includes(saved??'')?saved as typeof view.value:'computer')},{immediate:true})
+const view=ref<'computer'|'routines'>('computer')
+watch(()=>props.conversationId,()=>{let saved;try{saved=localStorage.getItem('yaoyao-bot-panel:'+props.conversationId)}catch{}view.value=saved==='routines'?'routines':'computer'},{immediate:true})
 watch(view,value=>{try{localStorage.setItem('yaoyao-bot-panel:'+props.conversationId,value)}catch{}})
 </script>
 <template>
  <aside class="bot-panel" aria-label="机器人右侧栏">
-  <header><div class="tabs" role="group" aria-label="机器人侧栏项目"><button v-if="!group" :aria-pressed="view==='computer'" @click="view='computer'"><AppIcon name="monitor" :size="15"/>电脑</button><button v-if="!group" :aria-pressed="view==='routines'" @click="view='routines'"><AppIcon name="calendar" :size="15"/>定时任务</button><button :aria-pressed="view==='inspector'" @click="view='inspector'"><AppIcon name="bug" :size="15"/>Inspector</button></div><button aria-label="关闭机器人侧栏" @click="emit('close')"><AppIcon name="close"/></button></header>
-  <LocalVmChatPanel v-if="view==='computer'&&!group" embedded :agents="agents" :is-admin="isAdmin" :active="active" @changed="emit('changed')" @settings="emit('settings')" @desktop="emit('desktop',$event)" @workspace="emit('workspace',$event)"/>
-  <WorkspaceRoutinesPanel v-else-if="view==='routines'&&agents[0]&&!group" :agent-id="agents[0].id"/>
-  <WorkspaceInspectorPanel v-else :conversation-id="conversationId" :task-id="taskId"/>
+  <header v-if="mode==='computer'"><div class="tabs" role="group" aria-label="电脑与定时任务"><button :aria-pressed="view==='computer'" @click="view='computer'"><AppIcon name="monitor" :size="15"/>电脑</button><button :aria-pressed="view==='routines'" @click="view='routines'"><AppIcon name="calendar" :size="15"/>定时任务</button></div><button class="close" aria-label="关闭机器人侧栏" @click="emit('close')"><AppIcon name="close"/></button></header>
+  <WorkspaceInspectorPanel v-if="mode==='inspector'" show-close :conversation-id="conversationId" :task-id="taskId" @close="emit('close')"/>
+  <LocalVmChatPanel v-else-if="view==='computer'" embedded :agents="agents" :is-admin="isAdmin" :active="active" @changed="emit('changed')" @settings="emit('settings')" @desktop="emit('desktop',$event)" @workspace="emit('workspace',$event)"/>
+  <WorkspaceRoutinesPanel v-else-if="agents[0]" :agent-id="agents[0].id"/>
  </aside>
 </template>
 <style scoped>
-.bot-panel{width:420px;min-width:320px;max-width:46%;height:100%;border-left:1px solid var(--line);background:var(--surface);display:flex;flex-direction:column;color:var(--text-primary)}header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line)}.tabs{display:flex;flex-wrap:wrap;gap:2px}.tabs button{font-size:12px;padding:8px}.tabs button[aria-pressed=true]{background:var(--surface-soft);color:var(--text-primary);font-weight:600}button{font:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;background:transparent;color:var(--text-secondary);border-radius:8px;min-width:44px;min-height:44px;cursor:pointer}button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media(max-width:900px){.bot-panel{position:fixed;inset:0 0 0 auto;width:min(440px,100vw);max-width:100%;z-index:35;box-shadow:-20px 0 60px #0002}}:deep(.local-vm-chat-panel.embedded){width:100%;max-width:none;min-width:0;border:0;position:static;box-shadow:none;flex:1;min-height:0}:deep(.local-vm-chat-panel.embedded>header){display:none}
+.bot-panel{width:420px;min-width:320px;max-width:46%;height:100%;border-left:1px solid var(--line);background:var(--surface);display:flex;flex-direction:column;color:var(--text-primary)}header{position:relative;display:flex;align-items:center;justify-content:center;min-height:64px;padding:10px 58px;border-bottom:1px solid var(--line);box-sizing:border-box}.tabs{display:flex;flex-wrap:wrap;justify-content:center;gap:2px}.tabs button{font-size:12px;padding:8px}.tabs button[aria-pressed=true]{background:var(--surface-soft);color:var(--text-primary);font-weight:600}.close{position:absolute;right:12px}button{font:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;background:transparent;color:var(--text-secondary);border-radius:8px;min-width:44px;min-height:44px;cursor:pointer}button:hover{background:var(--surface-hover);color:var(--text-primary)}button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media(max-width:900px){.bot-panel{position:fixed;inset:0 0 0 auto;width:min(440px,100vw);max-width:100%;z-index:35;box-shadow:-20px 0 60px #0002}}:deep(.local-vm-chat-panel.embedded){width:100%;max-width:none;min-width:0;border:0;position:static;box-shadow:none;flex:1;min-height:0}:deep(.local-vm-chat-panel.embedded>header){display:none}
 </style>

Разница между файлами не показана из-за своего большого размера
+ 3 - 2
src/client/components/workspace/WorkspaceInspectorPanel.vue


+ 31 - 8
src/client/views/ConversationsView.vue

@@ -191,7 +191,10 @@ let cursor = 0,
 const selected = computed(() => (typeof route.params.id === 'string' ? route.params.id : undefined))
 const selectedTask = computed(() => typeof route.query.taskId === 'string' ? route.query.taskId : '')
 const shell=ref<InstanceType<typeof WorkspaceShell>>(),vmWorkspace=ref<InstanceType<typeof LocalVmWorkspace>>(),desktopViewer=ref<InstanceType<typeof ComputerPanel>>()
-const computerOpen=ref(false),computerDockOpen=ref(false),twoDesktops=ref(false),desktopAgent=ref<Agent>()
+const computerOpen=ref(false),dockView=ref<'computer'|'inspector'>(),twoDesktops=ref(false),desktopAgent=ref<Agent>()
+const creatingTask=ref(false)
+const CREATE_TASK_VALUE='__create_task__'
+function toggleDock(view:'computer'|'inspector'){dockView.value=dockView.value===view?undefined:view}
 async function openComputer(agent:Agent){
   if(window.yaoyaoDesktop?.openComputer){try{await window.yaoyaoDesktop.openComputer(agent.id)}catch(cause){error.value=cause instanceof Error?cause.message:'无法打开电脑窗口'}return}
   desktopAgent.value=agent;computerOpen.value=true
@@ -320,6 +323,20 @@ async function markRead() {
   })
   read.readSeq = read.lastSeq
 }
+async function selectTask(event:Event){
+  const select=event.currentTarget as HTMLSelectElement,value=select.value
+  if(value!==CREATE_TASK_VALUE){await router.push({query:{...route.query,taskId:value}});return}
+  select.value=activeTask.value?.id??''
+  if(!active.value||active.value.kind!=='group'||creatingTask.value)return
+  const conversationId=active.value.id
+  creatingTask.value=true;error.value=''
+  try{
+    const result=await apiRequest<{task:WorkspaceTask}>(`/api/app/conversations/${conversationId}/tasks`,{method:'POST',body:{}})
+    if(active.value?.id!==conversationId)return
+    tasks.value=[result.task,...tasks.value.filter(task=>task.id!==result.task.id)]
+    await router.push({query:{...route.query,taskId:result.task.id}})
+  }catch(cause){error.value=cause instanceof Error?cause.message:'新建任务失败'}finally{creatingTask.value=false}
+}
 async function stopTask() {
   if (!active.value || !activeTask.value) return
   try {
@@ -653,6 +670,7 @@ watch([selected,selectedTask], () => {
   pendingRequestId = undefined;uploadedSources = [];uploadedReferences = []
   void load()
 })
+watch(selected,()=>{dockView.value=undefined})
 onMounted(async () => {
   void auth.refreshProfileAvatars().catch(() => undefined)
   try {
@@ -719,11 +737,16 @@ onBeforeUnmount(() => {
         <template #header-leading><button v-if="active" class="workspace-list-back icon-button" aria-label="返回 Bot 列表" @click="router.push('/conversations')"><AppIcon name="chevron-left" /></button></template>
         <template #header-actions>
           <div v-if="active" class="header-actions">
-            <button type="button" class="icon-button" :aria-label="active.kind==='group'?'Inspector':'电脑与定时任务'" :title="active.kind==='group'?'Inspector':'电脑与定时任务'" @click="computerDockOpen=!computerDockOpen"><AppIcon :name="active.kind==='group'?'tools':'monitor'"/></button>
-            <select v-if="active.kind === 'group' && tasks.length" class="task-picker" aria-label="当前任务" :value="activeTask?.id" @change="router.push({query:{...route.query,taskId:($event.target as HTMLSelectElement).value}})">
-              <option v-for="task in tasks" :key="task.id" :value="task.id">{{ task.title }}</option>
-            </select>
+            <button v-if="active.kind==='direct'" type="button" class="icon-button" aria-label="电脑与定时任务" title="电脑与定时任务" :aria-pressed="dockView==='computer'" @click="toggleDock('computer')"><AppIcon name="monitor"/></button>
+            <span v-if="active.kind === 'group' && tasks.length" class="task-picker-shell">
+              <select class="task-picker" aria-label="当前任务" :aria-busy="creatingTask" :disabled="creatingTask" :value="activeTask?.id" @change="selectTask">
+                <option v-for="task in tasks" :key="task.id" :value="task.id">{{ task.title }}</option>
+                <option :value="CREATE_TASK_VALUE">新建任务</option>
+              </select>
+              <AppIcon name="chevron-down" :size="14"/>
+            </span>
             <button class="icon-button" aria-label="聊天设置" title="聊天设置" @click="openDialog(active.kind === 'direct' ? 'editAgent' : 'editGroup')"><AppIcon name="settings" /></button>
+            <button type="button" class="icon-button" aria-label="Inspector" title="Inspector" :aria-pressed="dockView==='inspector'" @click="toggleDock('inspector')"><AppIcon name="bug"/></button>
           </div>
         </template>
       </MessageTimeline>
@@ -736,7 +759,7 @@ onBeforeUnmount(() => {
         :mention-options="active.kind === 'group' ? members.map(a => ({id:a.id,label:a.name,insertText:`@${a.name} `})) : []"
         @send="sendFromComposer" @stop="control('stop')" @tool-trace-toggle="showThinking = !showThinking" @clear-reference="quoted = null" @error="error = $event" />
     </section>
-    <WorkspaceBotPanel v-if="computerDockOpen&&active" :conversation-id="active.id" :task-id="activeTask?.id" :group="active.kind==='group'" :active="!twoDesktops" :agents="computerCandidates" :is-admin="auth.user?.role === 'admin'" @close="computerDockOpen=false" @changed="refresh()" @settings="shell?.openLocalVm()" @desktop="openComputer" @workspace="desktopAgent=$event;twoDesktops=true" />
+    <WorkspaceBotPanel v-if="dockView&&active" :conversation-id="active.id" :task-id="activeTask?.id" :mode="dockView" :active="!twoDesktops" :agents="computerCandidates" :is-admin="auth.user?.role === 'admin'" @close="dockView=undefined" @changed="refresh()" @settings="shell?.openLocalVm()" @desktop="openComputer" @workspace="desktopAgent=$event;twoDesktops=true" />
     </div>
     <ComputerPanel ref="desktopViewer" v-if="computerOpen&&desktopAgent" :agents="[desktopAgent]" auto-take @changed="load()" @close="computerOpen=false" />
     <LocalVmWorkspace ref="vmWorkspace" v-if="twoDesktops&&desktopAgent" :agents="agents.filter(a=>a.execution==='computer'&&!a.archived&&!a.computerEnvironmentId)" :primary="desktopAgent.id" @close="twoDesktops=false" />
@@ -915,9 +938,9 @@ onBeforeUnmount(() => {
   margin: 15px 0;
   font-size: 13px;
 }
-.task-picker { max-width:180px;min-height:36px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--text-primary);padding:4px 8px;font:inherit }
+.task-picker-shell{position:relative;display:inline-flex;align-items:center;max-width:210px;min-width:0}.task-picker-shell>.app-icon{position:absolute;right:11px;pointer-events:none;color:var(--text-muted)}.task-picker{width:100%;min-width:0;max-width:210px;min-height:38px;appearance:none;border:1px solid var(--line);border-radius:999px;background:var(--surface);color:var(--text-primary);padding:6px 34px 6px 14px;font:inherit;font-size:12px;font-weight:520;text-overflow:ellipsis;cursor:pointer;box-shadow:0 1px 2px color-mix(in srgb,var(--text-primary) 5%,transparent);transition:border-color 140ms ease,background-color 140ms ease,box-shadow 140ms ease}.task-picker:hover{border-color:var(--line-strong);background:var(--surface-hover)}.task-picker:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.task-picker:disabled{cursor:wait;opacity:.55}
 .task-queue-status{margin:0 20px 8px;font-size:13px;color:var(--text-muted)}
-@media(max-width:720px){.task-picker{max-width:100px}}
+@media(max-width:720px){.task-picker-shell,.task-picker{max-width:118px}.task-picker{min-height:44px;padding-left:12px}}
 .team-management-permission > span {
   display: flex;
   align-items: center;

+ 37 - 0
tests/client/workspacePresentation.test.ts

@@ -17,7 +17,9 @@ describe('workspace chat uses the established presentation', () => {
 })
 
 import { mount } from '@vue/test-utils'
+import { nextTick } from 'vue'
 import ConversationList from '@/components/workspace/ConversationList.vue'
+import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
 import TeamAvatar from '@/components/common/TeamAvatar.vue'
 import type { WorkspaceAgent, WorkspaceConversation } from '../../src/shared/workspace'
 
@@ -47,6 +49,41 @@ it('composes real member avatars and refreshes them without changing group membe
   wrapper.unmount()
 })
 
+it.each([
+  ['direct', '查看机器人'],
+  ['group', '查看群聊'],
+] as const)('opens a %s conversation from its actions menu with the expected label', async (kind, label) => {
+  const conversation: WorkspaceConversation = {
+    id: kind,
+    kind,
+    name: kind === 'direct' ? '机器人' : '群聊',
+    avatar: '',
+    memberIds: [],
+    instructions: '',
+    administratorId: '',
+    mode: 'host',
+    autoReplyIds: [],
+    maxReplyRounds: 1,
+    archived: false,
+    pinned: false,
+    readSeq: 0,
+    lastSeq: 0,
+    preview: '',
+    createdAt: 1,
+    updatedAt: 1,
+  }
+  const wrapper = mount(ConversationList, { attachTo: document.body, props: { conversations: [conversation] } })
+  wrapper.getComponent(ResourceSidebar).vm.$emit('more', conversation.id, new MouseEvent('click', { clientX: 100, clientY: 100 }))
+  await nextTick()
+  const menu = document.body.querySelector<HTMLElement>('[aria-label="聊天操作"]')!
+  const view = [...menu.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent === label)!
+  view.click()
+  await nextTick()
+  expect(wrapper.emitted('select')).toEqual([[conversation.id]])
+  expect(document.body.querySelector('[aria-label="聊天操作"]')).toBeNull()
+  wrapper.unmount()
+})
+
 
 it('keeps inline thinking in the process trace and its media out of the message body', () => {
   const messages = workspaceMessagesToUi([{

+ 13 - 2
tests/e2e/bot-panels.spec.ts

@@ -31,11 +31,15 @@ test('robot sidebar, source changes, requests, routines and group boundaries wor
  const panel=page.getByRole('complementary',{name:'机器人右侧栏'})
  await expect(panel.getByRole('tab',{name:'自动',exact:true})).toBeVisible()
  await expect(panel.getByRole('tab',{name:'云端 · Grok Bot',exact:true})).toBeVisible()
+ await expect(panel.getByRole('button',{name:'电脑',exact:true})).toHaveAttribute('aria-pressed','true')
+ await expect(panel.locator(':scope > header .tabs')).toHaveCSS('justify-content','center')
  await page.screenshot({path:evidence+'/desktop-computer.png'})
- await panel.getByRole('button',{name:'Inspector',exact:true}).click()
+ await page.getByRole('button',{name:'Inspector',exact:true}).click()
+ await expect(panel.locator(':scope > header')).toHaveCount(0)
  await panel.getByRole('button',{name:'Raw',exact:true}).click()
  await expect(panel.locator('summary').filter({hasText:'prompt.submit'}).first()).toBeVisible()
  await page.screenshot({path:evidence+'/desktop-inspector.png'})
+ await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  await panel.getByRole('button',{name:'定时任务',exact:true}).click()
  await panel.getByRole('button',{name:'新建定时任务',exact:true}).click()
  const routineEditor=page.getByRole('dialog',{name:'新建定时任务',exact:true})
@@ -60,11 +64,18 @@ test('robot sidebar, source changes, requests, routines and group boundaries wor
  const group=(await response.json()).conversation
  await page.goto('/conversations/'+group.id)
  await expect(page.getByRole('button',{name:'电脑与定时任务',exact:true})).toHaveCount(0)
+ const taskPicker=page.getByRole('combobox',{name:'当前任务',exact:true})
+ const optionCount=await taskPicker.locator('option').count()
+ await taskPicker.selectOption('__create_task__')
+ await expect(page).toHaveURL(/taskId=/)
+ await expect(page.getByRole('combobox',{name:'当前任务',exact:true}).locator('option')).toHaveCount(optionCount+1)
+ await page.screenshot({path:evidence+'/desktop-group-task-picker.png'})
  await page.getByRole('button',{name:'Inspector',exact:true}).click()
+ await expect(panel.locator(':scope > header')).toHaveCount(0)
  await expect(panel.getByRole('button',{name:'电脑',exact:true})).toHaveCount(0)
  await expect(panel.getByRole('button',{name:'定时任务',exact:true})).toHaveCount(0)
  await page.setViewportSize({width:375,height:812});await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  await page.screenshot({path:evidence+'/phone-group-inspector.png'})
  const box=await panel.boundingBox();expect(box!.width).toBeLessThanOrEqual(375)
- await expect(panel.getByRole('button',{name:'关闭机器人侧栏',exact:true})).toBeVisible()
+ await expect(panel.getByRole('button',{name:'关闭 Inspector',exact:true})).toBeVisible()
 })

Некоторые файлы не были показаны из-за большого количества измененных файлов