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

发布 v0.3.9:精简聊天侧栏并修正搜索与头像背景

LSamien 2 месяцев назад
Родитель
Сommit
7b29bbbb25

+ 1 - 1
README.md

@@ -1,6 +1,6 @@
 # 夭夭 Web
 
-当前发布版本:**v0.3.8**,配套 iOS **1.3(173)**。详见 [发布说明](docs/releases/v0.3.8.md)。
+当前发布版本:**v0.3.9**,配套 iOS **1.3(173)**。详见 [发布说明](docs/releases/v0.3.9.md)。
 
 夭夭 Web 为网页和 iOS 提供统一的 Agent、群聊、文件库及语音配置服务。Hermes 通过标准 9119 HTTP/WebSocket 接口执行任务;无需安装 Yaoyao 插件。
 

+ 16 - 0
docs/releases/v0.3.9.md

@@ -0,0 +1,16 @@
+# v0.3.9
+
+按浏览器批注精简聊天模块侧栏,并统一头像容器的透明背景。
+
+- 移除消息、侧栏、搜索结果及输入提示头像外层的黑色/灰色底板,保持头像本身和动态状态。
+- 聊天模块侧栏只保留顶部 Logo 与操作、搜索、聊天列表和底部账号/设置栏。
+- 展开时在折叠按钮右侧增加加号;折叠后加号位于 Logo 下方,菜单仅包含“新建 Bot”和“新建群聊”。
+- 搜索使用弹出层,提供“未归档”和“已归档”分类;切换分类保留关键词,搜索结果使用对应聊天的真实头像。
+- 支持折叠侧栏直接搜索、键盘关闭/焦点恢复和移动端抽屉。
+
+本次为 Web 界面更新,继续配套 iOS 1.3(173)。iOS 头像组件及直接容器已检查,没有同类额外底板。
+
+## 验证
+
+- 85 个测试文件、490 项测试,以及类型检查、生产构建通过。
+- 浏览器验证展开与折叠后的加号位置、两项创建菜单、搜索分类与焦点恢复、归档恢复、移动抽屉,以及浅色和深色主题下的透明头像容器。

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.3.8",
+  "version": "0.3.9",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "hermes-yaoyao",
-      "version": "0.3.8",
+      "version": "0.3.9",
       "license": "BSL-1.1",
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.3.8",
+  "version": "0.3.9",
   "private": true,
   "license": "BSL-1.1",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",

+ 3 - 3
release.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.3.8",
-  "webVersion": "0.3.8",
-  "gitTag": "v0.3.8"
+  "releaseVersion": "0.3.9",
+  "webVersion": "0.3.9",
+  "gitTag": "v0.3.9"
 }

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


+ 1 - 1
src/client/components/app/ResourceSidebar.vue

@@ -215,7 +215,7 @@ defineExpose({ focusSearch })
 .sidebar-item__expand { display: grid; width: 18px; height: 24px; flex: 0 0 18px; place-items: center; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--text-muted); cursor: pointer; }.sidebar-item__expand:hover, .sidebar-item__expand:focus-visible { outline: 0; background: var(--surface-hover); color: var(--text-primary); }.sidebar-item__expand :deep(.app-icon) { transition: transform 120ms ease; }.sidebar-item__expand--collapsed :deep(.app-icon) { transform: rotate(-90deg); }
 .sidebar-item__icon { position: relative; display: grid; place-items: center; width: 23px; height: 23px; flex: 0 0 23px; border: 0; border-radius: 7px; color: var(--text-muted); background: transparent; }
 .sidebar-item.active .sidebar-item__icon, .sidebar-item:hover .sidebar-item__icon { color: var(--text-secondary); }
-.sidebar-item__icon--avatar { background: var(--surface-hover); color: var(--text-secondary); font-size: 10px; font-weight: 700; }
+.sidebar-item__icon--avatar { background: transparent; color: var(--text-secondary); font-size: 10px; font-weight: 700; }
 .presence { position: absolute; right: -2px; bottom: -2px; width: 8px; height: 8px; border: 2px solid var(--surface); border-radius: 50%; background: var(--text-muted); }
 .presence--online { background: var(--success); }.presence--working { background: var(--warning); }.presence--offline { background: var(--text-muted); }
 .sidebar-item__copy { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 1px; }

+ 85 - 16
src/client/components/app/WorkspaceShell.vue

@@ -83,6 +83,8 @@ const emit = defineEmits<{
   selectProfile: [profile: string]
   saveIdentity: [input: ProfileIdentityInput]
   closeInspector: []
+  createAgent: []
+  createGroup: []
 }>()
 
 const route = useRoute()
@@ -97,6 +99,10 @@ const mobileDrawerClose = ref<HTMLButtonElement>()
 const profileMenuTrigger = ref<HTMLButtonElement>()
 const sidebarCollapsed = ref(false)
 const sidebarSearchOpen = ref(false)
+const createMenuOpen = ref(false)
+const createMenu = ref<HTMLElement | null>(null)
+let createTrigger: HTMLElement | null = null
+const createPosition = ref({ left: '8px', top: '58px' })
 const desktopSidebarContext = ref<HTMLElement | null>(null)
 const mobileSidebarContext = ref<HTMLElement | null>(null)
 
@@ -149,6 +155,40 @@ function closeMobileDrawer() {
 function closeMenus(event: MouseEvent) {
   const target = event.target as HTMLElement
   if (!target.closest('.sidebar-account-switcher')) profileMenuOpen.value = false
+  if (!target.closest('.workspace-create-menu, .sidebar-create-trigger')) createMenuOpen.value = false
+}
+function closeCreateMenu() {
+  createMenuOpen.value = false
+  void nextTick(() => { if (createTrigger?.isConnected) createTrigger.focus() })
+}
+async function openCreateMenu(event: Event) {
+  if (createMenuOpen.value) { closeCreateMenu(); return }
+  createTrigger = event.currentTarget as HTMLElement
+  const rect = createTrigger.getBoundingClientRect()
+  createPosition.value = { left: `${Math.max(8, Math.min(rect.left, window.innerWidth - 192))}px`, top: `${Math.max(8, Math.min(rect.bottom + 6, window.innerHeight - 96))}px` }
+  profileMenuOpen.value = false
+  createMenuOpen.value = true
+  await nextTick()
+  createMenu.value?.querySelector<HTMLButtonElement>('[role="menuitem"]')?.focus()
+}
+function chooseCreate(kind: 'agent' | 'group') {
+  closeCreateMenu()
+  mobileDrawerOpen.value = false
+  if (kind === 'agent') emit('createAgent')
+  else emit('createGroup')
+}
+function createMenuKeydown(event: KeyboardEvent) {
+  if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return
+  event.preventDefault()
+  const buttons = [...(createMenu.value?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]') ?? [])]
+  const index = buttons.indexOf(document.activeElement as HTMLButtonElement)
+  buttons[event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1 : (index + (event.key === 'ArrowUp' ? -1 : 1) + buttons.length) % buttons.length]?.focus()
+}
+function workspaceKeydown(event: KeyboardEvent) {
+  if (!applicationWorkspace.value || !(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'k' || settingsOpen.value || document.querySelector('dialog[open]')) return
+  event.preventDefault()
+  if (sidebarSearchOpen.value) document.dispatchEvent(new CustomEvent(SIDEBAR_SEARCH_CLOSE_EVENT))
+  else void openSidebarSearch(null)
 }
 
 function toggleProfileMenu(event: MouseEvent) {
@@ -207,6 +247,7 @@ function closeSettings() {
 }
 
 function toggleSidebar() {
+  createMenuOpen.value = false
   sidebarCollapsed.value = !sidebarCollapsed.value
   try { localStorage.setItem(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed.value ? '1' : '0') } catch { /* optional persistence */ }
 }
@@ -216,6 +257,12 @@ function navIcon(key: NavItem['key']): 'chat' | 'folder' | 'people' | 'board' {
 }
 
 async function openSidebarSearch(host: HTMLElement | null) {
+  if (applicationWorkspace.value) {
+    createMenuOpen.value = false
+    sidebarSearchOpen.value = true
+    document.dispatchEvent(new CustomEvent(SIDEBAR_SEARCH_EVENT, { detail: { section: 'groups' } }))
+    return
+  }
   if (sidebarCollapsed.value) {
     sidebarCollapsed.value = false
     try { localStorage.setItem(SIDEBAR_COLLAPSED_KEY, '0') } catch { /* optional persistence */ }
@@ -254,7 +301,7 @@ function handleSidebarFocusout(event: FocusEvent) {
 
 function handleSidebarSearchClosed() { sidebarSearchOpen.value = false }
 
-watch(() => route.fullPath, () => { mobileDrawerOpen.value = false })
+watch(() => route.fullPath, () => { mobileDrawerOpen.value = false; createMenuOpen.value = false })
 watch(() => activeNav.value.key, () => {
   sidebarSearchOpen.value = false
   profileMenuOpen.value = false
@@ -264,15 +311,17 @@ onMounted(() => {
   try { sidebarCollapsed.value = localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1' } catch { /* optional persistence */ }
   document.addEventListener('mousedown', closeMenus)
   document.addEventListener(SIDEBAR_SEARCH_CLOSE_EVENT, handleSidebarSearchClosed)
+  document.addEventListener('keydown', workspaceKeydown)
 })
 onBeforeUnmount(() => {
   document.removeEventListener('mousedown', closeMenus)
   document.removeEventListener(SIDEBAR_SEARCH_CLOSE_EVENT, handleSidebarSearchClosed)
+  document.removeEventListener('keydown', workspaceKeydown)
 })
 </script>
 
 <template>
-  <div class="workspace-shell" :class="{ 'workspace-shell--collapsed': sidebarCollapsed, 'workspace-shell--sidebar-focused': sidebarFocusMode }" :inert="settingsOpen">
+  <div class="workspace-shell" :class="{ 'workspace-shell--collapsed': sidebarCollapsed, 'workspace-shell--sidebar-focused': sidebarFocusMode, 'workspace-shell--conversations': applicationWorkspace }" :inert="settingsOpen || (applicationWorkspace && sidebarSearchOpen)">
     <header class="mobile-header" :inert="mobileDrawerOpen">
       <button ref="mobileNavigationTrigger" class="icon-button" type="button" aria-label="打开导航" @click="openMobileDrawer">
         <AppIcon name="menu" :size="20" />
@@ -307,27 +356,29 @@ onBeforeUnmount(() => {
         <AppIcon name="chevron-left" :size="18" />
       </button>
 
+      <button v-if="applicationWorkspace" class="sidebar-create-trigger" type="button" aria-label="新建" title="新建" aria-haspopup="menu" :aria-expanded="createMenuOpen" @click="openCreateMenu" @keydown.down.prevent="openCreateMenu"><AppIcon name="plus" :size="19" /></button>
+
       <button
         v-if="!['files', 'kanban'].includes(activeNav.key)"
         class="sidebar-search-trigger"
-        :class="{ 'sidebar-search-trigger--searching': sidebarSearchOpen }"
+        :class="{ 'sidebar-search-trigger--searching': sidebarSearchOpen && !applicationWorkspace }"
         type="button"
         aria-label="搜索"
         title="搜索"
         :aria-expanded="sidebarSearchOpen"
-        :aria-hidden="sidebarSearchOpen ? 'true' : undefined"
-        :tabindex="sidebarSearchOpen ? -1 : 0"
+        :aria-hidden="sidebarSearchOpen && !applicationWorkspace ? 'true' : undefined"
+        :tabindex="sidebarSearchOpen && !applicationWorkspace ? -1 : 0"
         @click="openSidebarSearch(desktopSidebarContext)"
       >
         <YaoYaoSidebarIcon name="search" />
         <span>搜索</span>
       </button>
 
-      <div v-if="hasPrimaryAction" class="sidebar-primary-action">
+      <div v-if="hasPrimaryAction && !applicationWorkspace" class="sidebar-primary-action">
         <slot name="sidebar-action" />
       </div>
 
-      <nav class="sidebar-feature-nav" aria-label="功能入口">
+      <nav v-if="!applicationWorkspace" class="sidebar-feature-nav" aria-label="功能入口">
         <button
           v-for="item in featureNavItems"
           :key="item.key"
@@ -409,32 +460,32 @@ onBeforeUnmount(() => {
         <button class="sidebar-brand" type="button" aria-label="返回聊天" @click="navigate('/conversations')">
           <BrandMark :size="32" compact />
         </button>
-        <button ref="mobileDrawerClose" class="icon-button" type="button" aria-label="关闭导航" @click="closeMobileDrawer">
+        <div class="mobile-drawer__actions"><button ref="mobileDrawerClose" class="icon-button" type="button" aria-label="关闭导航" @click="closeMobileDrawer">
           <AppIcon name="close" />
-        </button>
+        </button><button v-if="applicationWorkspace" class="sidebar-create-trigger" type="button" aria-label="新建" title="新建" aria-haspopup="menu" :aria-expanded="createMenuOpen" @click="openCreateMenu"><AppIcon name="plus" :size="19" /></button></div>
       </div>
 
       <button
         v-if="!['files', 'kanban'].includes(activeNav.key)"
         class="sidebar-search-trigger"
-        :class="{ 'sidebar-search-trigger--searching': sidebarSearchOpen }"
+        :class="{ 'sidebar-search-trigger--searching': sidebarSearchOpen && !applicationWorkspace }"
         type="button"
         aria-label="搜索"
         title="搜索"
         :aria-expanded="sidebarSearchOpen"
-        :aria-hidden="sidebarSearchOpen ? 'true' : undefined"
-        :tabindex="sidebarSearchOpen ? -1 : 0"
+        :aria-hidden="sidebarSearchOpen && !applicationWorkspace ? 'true' : undefined"
+        :tabindex="sidebarSearchOpen && !applicationWorkspace ? -1 : 0"
         @click="openSidebarSearch(mobileSidebarContext)"
       >
         <AppIcon name="search" :size="18" />
         <span>搜索</span>
       </button>
 
-      <div v-if="hasPrimaryAction" class="sidebar-primary-action">
+      <div v-if="hasPrimaryAction && !applicationWorkspace" class="sidebar-primary-action">
         <slot name="sidebar-action" />
       </div>
 
-      <nav class="sidebar-feature-nav" aria-label="功能入口">
+      <nav v-if="!applicationWorkspace" class="sidebar-feature-nav" aria-label="功能入口">
         <button
           v-for="item in featureNavItems"
           :key="item.key"
@@ -538,6 +589,14 @@ onBeforeUnmount(() => {
       @save-identity="emit('saveIdentity', $event)"
       @set-theme="emit('setTheme', $event)"
     />
+    <Teleport to="body">
+      <div v-if="createMenuOpen" class="workspace-create-dismiss" @pointerdown.self="closeCreateMenu" @keydown.esc.prevent.stop="closeCreateMenu">
+        <div ref="createMenu" class="workspace-create-menu" :style="createPosition" role="menu" aria-label="新建聊天" @keydown="createMenuKeydown">
+          <button type="button" role="menuitem" @click="chooseCreate('agent')"><AppIcon name="users" :size="17" />新建 Bot</button>
+          <button type="button" role="menuitem" @click="chooseCreate('group')"><AppIcon name="groups" :size="17" />新建群聊</button>
+        </div>
+      </div>
+    </Teleport>
   </div>
 </template>
 
@@ -669,7 +728,7 @@ onBeforeUnmount(() => {
 .sidebar-context__body :deep(.sidebar-item.sidebar-item--single-line) { min-height: 31px; padding: 1px 7px; }
 .sidebar-context__body :deep(.sidebar-item--single-line .sidebar-item__row strong) { font-size: 14px; font-weight: 400; }
 .sidebar-context__body :deep(.sidebar-item__icon) { width: 25px; height: 25px; flex-basis: 25px; border: 0; border-radius: 7px; background: transparent; }
-.sidebar-context__body :deep(.sidebar-item__icon--avatar) { background: var(--surface-soft); color: var(--text-secondary); }
+.sidebar-context__body :deep(.sidebar-item__icon--avatar) { background: transparent; color: var(--text-secondary); }
 .sidebar-context__body :deep(.sidebar-item__row strong) { font-size: 13px; }
 .sidebar-context__body :deep(.sidebar-item.sidebar-item--topic .sidebar-item__row strong) { font-size: 14px; font-weight: 450; }
 .sidebar-context__body :deep(.library-sidebar) { padding-top: 1px; }
@@ -690,7 +749,7 @@ onBeforeUnmount(() => {
 .sidebar-settings-trigger { display: grid; width: 38px; min-width: 38px; height: 38px; flex: 0 0 38px; place-items: center; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; }
 .sidebar-settings-trigger:hover { background: var(--surface-soft); color: var(--text-primary); }
 .sidebar-settings-trigger:focus-visible { outline: 0; box-shadow: inset 0 0 0 1px var(--line-strong), 0 0 0 3px var(--focus-ring); }
-.sidebar-account__avatar { display: grid; width: 30px; height: 30px; flex: 0 0 30px; place-items: center; border-radius: 50%; background: #c91e55; color: #fff; font-size: 13px; font-weight: 700; }
+.sidebar-account__avatar { display: grid; width: 30px; height: 30px; flex: 0 0 30px; place-items: center; border-radius: 50%; background: transparent; color: var(--text-secondary); font-size: 13px; font-weight: 700; }
 .sidebar-account-switcher__chevron { flex: 0 0 auto; color: var(--text-muted); }
 .profile-menu { position: absolute; right: 0; bottom: calc(100% + 6px); left: 0; padding: 5px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-raised); box-shadow: var(--shadow-float); }
 .profile-menu__heading { display: block; padding: 8px 8px 5px; color: var(--text-muted); font-size: 11px; font-weight: 650; }
@@ -757,4 +816,14 @@ onBeforeUnmount(() => {
 
 .drawer-fade-enter-active, .drawer-fade-leave-active { transition: opacity 160ms ease; }
 .drawer-fade-enter-from, .drawer-fade-leave-to { opacity: 0; }
+.sidebar-create-trigger{display:grid;width:34px;height:34px;flex:0 0 34px;place-items:center;padding:0;border:0;border-radius:9px;background:transparent;color:var(--text-muted);cursor:pointer}
+.sidebar-create-trigger:hover,.sidebar-create-trigger[aria-expanded="true"]{background:var(--surface-hover);color:var(--text-primary)}
+.workspace-shell--conversations .desktop-sidebar:not(.desktop-sidebar--collapsed) .sidebar-collapse{right:49px}
+.desktop-sidebar > .sidebar-create-trigger{position:absolute;top:12px;right:11px}
+.desktop-sidebar--collapsed > .sidebar-create-trigger{position:static;width:42px;height:36px;flex-basis:36px;align-self:center;margin:0 auto 4px}
+.mobile-drawer__actions{display:flex;align-items:center;gap:4px}
+.workspace-create-dismiss{position:fixed;inset:0;z-index:80}
+.workspace-create-menu{position:absolute;width:180px;padding:5px;border:1px solid var(--line);border-radius:11px;background:var(--surface-raised);box-shadow:var(--shadow-float)}
+.workspace-create-menu button{display:flex;width:100%;min-height:36px;align-items:center;gap:9px;padding:7px 10px;border:0;border-radius:7px;background:transparent;color:var(--text-primary);font:13px var(--font-ui);text-align:left;cursor:pointer}
+.workspace-create-menu button:hover,.workspace-create-menu button:focus-visible{outline:0;background:var(--surface-hover)}
 </style>

+ 1 - 1
src/client/components/common/AgentAvatar.vue

@@ -90,7 +90,7 @@ const mouthPath = computed(() => {
 </template>
 
 <style scoped>
-.agent-avatar{display:block;position:relative;flex:0 0 auto;overflow:visible;line-height:1}.agent-avatar img,.agent-avatar svg{display:block;width:100%;height:100%;object-fit:cover}.agent-avatar--image img{border-radius:22%}.agent-avatar__body{transform-box:fill-box;transform-origin:center}.agent-avatar__eye{transform-box:fill-box;transform-origin:center;animation:agent-avatar-blink 9s ease-in-out infinite}.agent-avatar__eye--right{animation-delay:32ms}.agent-avatar--working .agent-avatar__body,.agent-avatar--working img{animation:agent-avatar-working .9s ease-in-out infinite}.agent-avatar--working .agent-avatar__eye{animation-duration:4.1s}.agent-avatar--notifying .agent-avatar__body,.agent-avatar--notifying img{animation:agent-avatar-notifying .7s cubic-bezier(.2,.8,.2,1) 2}.agent-avatar--notifying::after{position:absolute;inset:-3px;border:2px solid v-bind('mascot.color');border-radius:50%;content:"";animation:agent-avatar-ring 1.2s ease-out 2;pointer-events:none}.agent-avatar--waiting .agent-avatar__body,.agent-avatar--waiting img{animation:agent-avatar-waiting 1.8s ease-in-out infinite}.agent-avatar--waiting .agent-avatar__eye{animation-duration:3.4s}.agent-avatar--success .agent-avatar__body,.agent-avatar--success img{animation:agent-avatar-success .9s cubic-bezier(.18,.8,.2,1) both}.agent-avatar--failure .agent-avatar__body,.agent-avatar--failure img{animation:agent-avatar-failure .7s ease-in-out both}
+.agent-avatar{display:block;position:relative;flex:0 0 auto;overflow:visible;line-height:1;background:transparent}.agent-avatar img,.agent-avatar svg{display:block;width:100%;height:100%;object-fit:cover}.agent-avatar--image img{border-radius:22%}.agent-avatar__body{transform-box:fill-box;transform-origin:center}.agent-avatar__eye{transform-box:fill-box;transform-origin:center;animation:agent-avatar-blink 9s ease-in-out infinite}.agent-avatar__eye--right{animation-delay:32ms}.agent-avatar--working .agent-avatar__body,.agent-avatar--working img{animation:agent-avatar-working .9s ease-in-out infinite}.agent-avatar--working .agent-avatar__eye{animation-duration:4.1s}.agent-avatar--notifying .agent-avatar__body,.agent-avatar--notifying img{animation:agent-avatar-notifying .7s cubic-bezier(.2,.8,.2,1) 2}.agent-avatar--notifying::after{position:absolute;inset:-3px;border:2px solid v-bind('mascot.color');border-radius:50%;content:"";animation:agent-avatar-ring 1.2s ease-out 2;pointer-events:none}.agent-avatar--waiting .agent-avatar__body,.agent-avatar--waiting img{animation:agent-avatar-waiting 1.8s ease-in-out infinite}.agent-avatar--waiting .agent-avatar__eye{animation-duration:3.4s}.agent-avatar--success .agent-avatar__body,.agent-avatar--success img{animation:agent-avatar-success .9s cubic-bezier(.18,.8,.2,1) both}.agent-avatar--failure .agent-avatar__body,.agent-avatar--failure img{animation:agent-avatar-failure .7s ease-in-out both}
 @keyframes agent-avatar-blink{0%,42%,47%,100%{transform:scaleY(1)}44.5%{transform:scaleY(.08)}}
 @keyframes agent-avatar-working{0%,100%{transform:translateY(1px) rotate(-1deg) scale(.99)}50%{transform:translateY(-2px) rotate(1.5deg) scale(1.02)}}
 @keyframes agent-avatar-notifying{0%,100%{transform:translateY(0) rotate(0) scale(1)}25%{transform:translateY(2px) rotate(-3deg) scale(1.08,.92)}55%{transform:translateY(-4px) rotate(3deg) scale(.95,1.08)}78%{transform:translateY(0) rotate(-1deg) scale(1.03,.98)}}

+ 1 - 1
src/client/components/composer/ComposerShell.vue

@@ -640,7 +640,7 @@ defineExpose({
 .composer-menu button { display: flex; width: 100%; min-height: 42px; align-items: center; gap: 9px; padding: 5px 8px; border: 0; border-radius: 8px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
 .composer-menu button.active, .composer-menu button:hover { background: var(--surface-hover); }
 .composer-menu button:disabled { cursor: not-allowed; opacity: .48; }
-.composer-menu__avatar { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; background: var(--surface-soft); color: var(--text-secondary); font-size: 11px; }
+.composer-menu__avatar { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; background: transparent; color: var(--text-secondary); font-size: 11px; }
 .composer-menu button > span:nth-child(2) { display: flex; min-width: 0; flex: 1; flex-direction: column; }.composer-menu strong { font-size: 11px; }.composer-menu small { overflow: hidden; color: var(--text-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }.composer-menu em { color: var(--text-muted); font-size: 9px; font-style: normal; }
 .composer-drop-hint { position: absolute; z-index: 25; inset: 6px; display: flex; align-items: center; justify-content: center; gap: 8px; border: 1px dashed var(--line-strong); border-radius: 13px; background: color-mix(in srgb, var(--chat-composer-bg) 90%, transparent); color: var(--text-secondary); font-size: 12px; backdrop-filter: blur(5px); pointer-events: none; }
 .composer-error { position: absolute; right: max(24px, calc((100% - 760px) / 2)); bottom: calc(100% - 8px); max-width: min(420px, calc(100% - 32px)); margin: 0; padding: 7px 10px; border-radius: 8px; background: var(--danger); color: #fff; box-shadow: var(--shadow-float); font-size: 10px; }

+ 1 - 1
src/client/components/messages/MessageTimeline.vue

@@ -409,7 +409,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
 .timeline-frame--transparent .message-stack { padding-top: 58px; }
 .load-older { display: block; margin: 0 auto 18px; padding: 5px 9px; border: 1px solid var(--line); border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 10px; }.load-older:hover { background: var(--surface-soft); color: var(--text-secondary); }
 .message { position: relative; display: flex; max-width: 100%; gap: 10px; margin: 0 0 21px; }
-.message__avatar { display: grid; place-items: center; width: 27px; height: 27px; flex: 0 0 27px; border-radius: 9px; background: var(--accent); color: var(--text-on-solid); font-size: 11px; font-weight: 700; }
+.message__avatar { display: grid; place-items: center; width: 27px; height: 27px; flex: 0 0 27px; background: transparent; color: var(--text-secondary); font-size: 11px; font-weight: 700; }
 .message__body { position: relative; min-width: 0; max-width: min(680px, calc(100% - 37px)); }
 .message__meta { display: flex; min-height: 20px; align-items: center; gap: 7px; color: var(--text-muted); font-size: 10px; }.message__meta strong { display: inline-flex; align-items: center; color: var(--text-primary); font-size: 12px; font-weight: 600; }.message__remote-agent { margin-right: 4px; color: var(--accent); }.message__meta span { color: var(--warning); }.message__meta .message__execution { overflow: hidden; max-width: 230px; color: var(--text-secondary); font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
 .message__content { color: var(--text-primary); }

+ 6 - 17
src/client/components/workspace/ConversationList.vue

@@ -1,24 +1,14 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
-import type { SidebarItem } from '@/components/app/types'
-import { workspaceAvatarMembers, workspaceAvatarState } from './viewModels'
-import { formatMessageTime } from '@/utils/messageTime'
+import { workspaceConversationItem } from './viewModels'
 import type { WorkspaceAgent, WorkspaceConversation } from '@shared/workspace'
 const props = withDefaults(defineProps<{ conversations: WorkspaceConversation[]; agents?: WorkspaceAgent[]; selected?: string }>(), { agents: () => [] })
 const emit = defineEmits<{ select: [id: string]; pin: [id: string]; archive: [id: string] }>()
-const query = ref(''), archived = ref(false), menuId = ref('')
+const menuId = ref('')
 const menuPosition = ref({ left: '8px', top: '8px' })
 const menuConversation = computed(() => props.conversations.find(c => c.id === menuId.value))
-const rows = computed<SidebarItem[]>(() => props.conversations.filter(c => c.archived === archived.value &&
-  (!query.value || `${c.name} ${c.preview}`.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())))
-  .map(c => ({ id: c.id, title: c.name, subtitle: c.preview || '开始聊天', pinned: c.pinned,
-    section: c.pinned ? '已置顶' : '聊天', avatar: c.kind === 'group' ? '' : c.avatar,
-    avatarMembers: c.kind === 'group' ? workspaceAvatarMembers(c.memberIds, props.agents, c) : [],
-    meta: formatMessageTime(c.lastMessageAt ?? c.createdAt),
-    avatarKind: c.kind === 'direct' ? 'agent' : 'team',
-    avatarState: workspaceAvatarState(c, c.memberIds[0] || ''),
-    unread: c.unreadCount ?? Math.max(0, c.lastSeq - c.readSeq), status: c.activeRunId ? 'working' : undefined })))
+const rows = computed(() => props.conversations.filter(c => !c.archived).map(c => workspaceConversationItem(c, props.agents)))
 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 - 100))}px` }
@@ -31,10 +21,9 @@ function action(kind: 'pin' | 'archive') {
 </script>
 <template>
   <div class="conversation-list">
-    <label class="archive-filter"><input v-model="archived" type="checkbox" />显示已归档</label>
-    <ResourceSidebar :items="rows" :active-id="selected" :search="query" search-placeholder="搜索聊天"
+    <ResourceSidebar :items="rows" :active-id="selected" :searchable="false" external-search
       empty-title="还没有聊天" empty-description="创建 Agent,或选择成员新建群聊。"
-      @search="query = $event" @select="emit('select', $event)" @more="openMenu" @context-menu="openMenu" />
+      @select="emit('select', $event)" @more="openMenu" @context-menu="openMenu" />
     <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="聊天操作">
@@ -46,6 +35,6 @@ function action(kind: 'pin' | 'archive') {
   </div>
 </template>
 <style scoped>
-.conversation-list{display:flex;flex:1;flex-direction:column;min-height:0}.archive-filter{display:flex;align-items:center;gap:6px;padding:8px 12px;color:var(--text-muted);font-size:11px}
+.conversation-list{display:flex;flex:1;flex-direction:column;min-height:0}
 .conversation-menu-dismiss{position:fixed;inset:0;z-index:200}.conversation-actions{position:absolute;display:grid;min-width:155px;padding:5px;border:1px solid var(--line);border-radius:10px;background:var(--surface-raised);box-shadow:var(--shadow-float)}.conversation-actions button{padding:9px 12px;border:0;border-radius:7px;background:transparent;color:var(--text-primary);text-align:left;cursor:pointer;font-size:12px}.conversation-actions button:hover{background:var(--surface-hover)}
 </style>

+ 13 - 0
src/client/components/workspace/viewModels.ts

@@ -17,6 +17,7 @@ import type { UiAgent, UiRoom } from '@/components/groups/types'
 import type { UiLibraryItem } from '@/components/library/types'
 import type { UiInteraction, UiMessage, UiMessageAttachment, UiToolCall } from '@/components/messages/types'
 import { normalizeAssistantMediaMarkdown } from '@/utils/mediaMarkdown'
+import { formatMessageTime } from '@/utils/messageTime'
 
 function record(value: unknown): Record<string, unknown> {
   return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
@@ -405,3 +406,15 @@ export function workspaceAvatarState(conversation: import('@shared/workspace').W
   if (!conversation?.activeRunId || conversation.activeAgentId !== agentId) return 'idle'
   return conversation.activeRunStatus === 'waiting' || conversation.activeRunStatus === 'uncertain' ? 'waiting' : 'working'
 }
+
+export function workspaceConversationItem(c: import('@shared/workspace').WorkspaceConversation, agents: import('@shared/workspace').WorkspaceAgent[]): SidebarItem {
+  return {
+    id: c.id, title: c.name, subtitle: c.preview || '开始聊天', pinned: c.pinned,
+    section: c.pinned ? '已置顶' : '聊天', avatar: c.kind === 'group' ? '' : c.avatar,
+    avatarMembers: c.kind === 'group' ? workspaceAvatarMembers(c.memberIds, agents, c) : [],
+    meta: formatMessageTime(c.lastMessageAt ?? c.createdAt),
+    avatarKind: c.kind === 'direct' ? 'agent' : 'team',
+    avatarState: workspaceAvatarState(c, c.memberIds[0] || ''),
+    unread: c.unreadCount ?? Math.max(0, c.lastSeq - c.readSeq), status: c.activeRunId ? 'working' : undefined,
+  }
+}

+ 10 - 19
src/client/views/ConversationsView.vue

@@ -3,7 +3,8 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch }
 import { useRoute, useRouter } from 'vue-router'
 import WorkspaceShell from '@/components/app/WorkspaceShell.vue'
 import ConversationList from '@/components/workspace/ConversationList.vue'
-import WorkspaceNodesPanel from '@/components/workspace/WorkspaceNodesPanel.vue'
+import FloatingResourceSearch from '@/components/app/FloatingResourceSearch.vue'
+import type { SidebarItem } from '@/components/app/types'
 import AgentAvatar from '@/components/common/AgentAvatar.vue'
 import { createUuid } from '@/utils/id'
 import MessageTimeline from '@/components/messages/MessageTimeline.vue'
@@ -20,7 +21,7 @@ import TeamPresetPicker from '@/components/workspace/TeamPresetPicker.vue'
 import { TEAM_PRESETS, type TeamPreset } from '@/components/groups/teamPresets'
 import type { WorkspaceMemberRole } from '@shared/workspace'
 import AppIcon from '@/components/common/AppIcon.vue'
-import { workspaceAvatarMembers, workspaceAvatarState, workspaceMessagesToUi } from '@/components/workspace/viewModels'
+import { workspaceAvatarMembers, workspaceAvatarState, workspaceConversationItem, workspaceMessagesToUi } from '@/components/workspace/viewModels'
 import { useAuthStore } from '@/stores/auth'
 import { useThemeStore } from '@/stores/theme'
 import { apiRequest } from '@/api/client'
@@ -54,7 +55,10 @@ const text = ref(''),
   files = ref<WorkspaceFile[]>([]),
   mentions = ref<string[]>([]),
   older = ref(true)
-const showNodes = ref(false)
+const searchItems = computed<SidebarItem[]>(() => [false, true].map(archived => {
+  const items = conversations.value.filter(c => c.archived === archived).map(c => workspaceConversationItem(c, agents.value))
+  return { id: archived ? 'archived' : 'unarchived', title: archived ? '已归档' : '未归档', children: items, emptyText: archived ? '没有已归档聊天' : '没有未归档聊天' }
+}))
 const composer = ref<InstanceType<typeof ComposerShell>>()
 const timeline = ref<InstanceType<typeof MessageTimeline>>()
 const quoted = ref<UiMessage | null>(null)
@@ -500,14 +504,9 @@ onBeforeUnmount(() => {
     @select-profile="auth.selectProfile"
     @toggle-theme="theme.toggle"
     @set-theme="theme.setTheme"
+    @create-agent="openDialog('agent')"
+    @create-group="openDialog('group')"
   >
-    <template #sidebar-action
-      ><div class="new-actions">
-        <button @click="openDialog('agent')">+ 创建 Agent</button
-        ><button @click="openDialog('group')">+ 创建群聊</button
-        ><button @click="showNodes = true">节点</button>
-      </div></template
-    >
     <template #sidebar
       ><ConversationList :conversations="conversations" :agents="agents" :selected="selected" @select="select" @pin="action('pin', $event)" @archive="action('archive', $event)"
     /></template>
@@ -543,17 +542,9 @@ 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>
+    <FloatingResourceSearch section="groups" label="搜索聊天" :items="searchItems" tabbed @select="select" />
     <PreviewModal v-if="preview" :item="preview" :items="media" @close="preview = null" />
     <ImagePreviewLightbox v-model="mediaIndex" :images="lightboxMedia" />
-    <div
-      v-if="showNodes"
-      class="nodes-overlay"
-      role="dialog"
-      aria-modal="true"
-      aria-label="节点管理"
-    >
-      <div><button @click="showNodes = false">关闭</button><WorkspaceNodesPanel /></div>
-    </div>
     <dialog v-if="dialog" ref="dialogElement" class="editor" @cancel.prevent="closeDialog">
       <form @submit.prevent="save">
         <header>

+ 27 - 0
tests/client/floatingResourceSearch.test.ts

@@ -69,3 +69,30 @@ describe('FloatingResourceSearch', () => {
     wrapper.unmount()
   })
 })
+
+it('keeps the query across archived categories and renders direct Agent portraits rather than a group cluster', async () => {
+  const trigger = document.createElement('button')
+  document.body.append(trigger); trigger.focus()
+  const wrapper = mount(FloatingResourceSearch, { attachTo: document.body, props: {
+    section: 'groups', label: '搜索聊天', tabbed: true,
+    items: [
+      {id:'open',title:'未归档',children:[{id:'a',title:'测试角色',avatarKind:'agent',avatar:'yaoyao-mascot:v1:circle:e78531:friendly'}]},
+      {id:'archive',title:'已归档',children:[{id:'b',title:'测试归档',avatarKind:'agent',avatar:'yaoyao-mascot:v1:square:377fe6:friendly'}]},
+    ],
+  } })
+  document.dispatchEvent(new CustomEvent('hermes-yaoyao:sidebar-search', {detail:{section:'groups'}}))
+  await wrapper.vm.$nextTick()
+  const input = document.querySelector<HTMLInputElement>('[role="dialog"] input')!
+  input.value = '测试'; input.dispatchEvent(new Event('input', {bubbles:true}))
+  await wrapper.vm.$nextTick()
+  document.querySelector<HTMLButtonElement>('[role="tab"][aria-label="已归档"]')!.click()
+  await wrapper.vm.$nextTick()
+  expect(input.value).toBe('测试')
+  expect(document.querySelector('[role="option"]')?.textContent).toContain('测试归档')
+  expect(document.querySelector('[role="dialog"] .agent-avatar')).not.toBeNull()
+  expect(document.querySelector('[role="dialog"] .team-avatar')).toBeNull()
+  document.querySelector<HTMLButtonElement>('[aria-label="关闭搜索"]')!.click()
+  await wrapper.vm.$nextTick()
+  expect(document.activeElement).toBe(trigger)
+  wrapper.unmount()
+})

+ 23 - 2
tests/client/workspaceShell.test.ts

@@ -20,17 +20,18 @@ const SettingsCenterDialogStub = defineComponent({
   template: '<div v-if="open" data-testid="settings-center" :data-page="initialPage"><button data-testid="close-settings" type="button" @click="$emit(\'close\')">close</button></div>',
 })
 
-async function mountShell() {
+async function mountShell(start = '/chat') {
   const router = createRouter({
     history: createMemoryHistory(),
     routes: [
       { path: '/chat', component: { template: '<div></div>' } },
+      { path: '/conversations', component: { template: '<div></div>' } },
       { path: '/groups', component: { template: '<div></div>' } },
       { path: '/kanban', component: { template: '<div></div>' } },
       { path: '/files', component: { template: '<div></div>' } },
     ],
   })
-  await router.push('/chat')
+  await router.push(start)
   await router.isReady()
   return mount(WorkspaceShell, {
     attachTo: document.body,
@@ -131,3 +132,23 @@ describe('Workspace shell account controls', () => {
     wrapper.unmount()
   })
 })
+
+it('keeps the conversation sidebar focused with a two-item create menu and modal search while collapsed', async () => {
+  const wrapper = await mountShell('/conversations')
+  const rail = wrapper.get('.desktop-sidebar')
+  expect(rail.find('.sidebar-feature-nav').exists()).toBe(false)
+  expect(rail.find('.sidebar-primary-action').exists()).toBe(false)
+  expect(rail.find('.sidebar-footer').exists()).toBe(true)
+  await rail.get('.sidebar-create-trigger').trigger('click')
+  const items = document.querySelectorAll<HTMLButtonElement>('.workspace-create-menu [role="menuitem"]')
+  expect([...items].map(item => item.textContent?.trim())).toEqual(['新建 Bot', '新建群聊'])
+  items[0]?.click()
+  await wrapper.vm.$nextTick()
+  expect(wrapper.emitted('createAgent')).toHaveLength(1)
+  await rail.get('.sidebar-collapse').trigger('click')
+  await rail.get('.sidebar-search-trigger').trigger('click')
+  expect(wrapper.classes()).toContain('workspace-shell--collapsed')
+  document.dispatchEvent(new CustomEvent('hermes-yaoyao:sidebar-search-close'))
+  await wrapper.vm.$nextTick()
+  wrapper.unmount()
+})

+ 65 - 8
tests/e2e/workspace-chat.spec.ts

@@ -6,8 +6,12 @@ async function login(page: Page) {
   await page.getByRole('button', { name: '登录', exact: true }).click()
   await expect(page.getByRole('heading', { name: '还没有聊天' })).toBeVisible()
 }
+async function openCreate(page: Page, name: '新建 Bot' | '新建群聊') {
+  await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
+  await page.getByRole('menuitem', { name, exact: true }).click()
+}
 async function createAgent(page: Page, name: string) {
-  await page.getByRole('button', { name: '+ 创建 Agent', exact: true }).click()
+  await openCreate(page, '新建 Bot')
   const dialog = page.getByRole('dialog')
   await dialog.getByRole('textbox', { name: '名称', exact: true }).fill(name)
   await dialog
@@ -26,13 +30,45 @@ test('created roles and editable teams share a durable chat list without plugin
   page.on('request', (r) => {
     if (r.url().includes('/plugins/yaoyao')) pluginCalls.push(r.url())
   })
+  await page.setViewportSize({ width: 1638, height: 874 })
   await login(page)
+  const rail = page.locator('.desktop-sidebar')
+  await expect(rail.locator('.sidebar-feature-nav')).toHaveCount(0)
+  await expect(rail.locator('.sidebar-primary-action')).toHaveCount(0)
+  await expect(rail.getByRole('checkbox', { name: '显示已归档' })).toHaveCount(0)
+  const plus = rail.getByRole('button', { name: '新建', exact: true })
+  const collapse = rail.getByRole('button', { name: '折叠侧边栏', exact: true })
+  expect((await plus.boundingBox())!.x).toBeGreaterThan((await collapse.boundingBox())!.x)
+  await collapse.click()
+  await expect(rail).toHaveClass(/desktop-sidebar--collapsed/)
+  const logoBox = (await rail.locator('.sidebar-brand').boundingBox())!
+  const plusBox = (await plus.boundingBox())!
+  expect(plusBox.y).toBeGreaterThanOrEqual(logoBox.y + logoBox.height)
+  await plus.click()
+  await expect(page.getByRole('menu', { name: '新建聊天' }).getByRole('menuitem')).toHaveCount(2)
+  await page.screenshot({ path: testInfo.outputPath('collapsed-create-menu.png'), fullPage: true })
+  await page.keyboard.press('Escape')
+  await rail.getByRole('button', { name: '搜索', exact: true }).click()
+  await expect(page.getByRole('dialog', { name: '搜索聊天' })).toBeVisible()
+  await expect(rail).toHaveClass(/desktop-sidebar--collapsed/)
+  await page.keyboard.press('Escape')
+  await expect(rail.getByRole('button', { name: '搜索', exact: true })).toBeFocused()
+  await rail.getByRole('button', { name: '展开侧边栏', exact: true }).click()
   await page.addInitScript(() => { Object.defineProperty(crypto, 'randomUUID', { value: undefined, configurable: true }) })
   await page.reload()
   await createAgent(page, '产品经理')
   await page.getByPlaceholder('输入消息,Enter 发送,Shift + Enter 换行').fill('请说明角色职责')
   await page.getByRole('button', { name: '发送消息', exact: true }).click()
   await expect(page.locator('.message--assistant')).toContainText('我是产品经理')
+  for (const colorScheme of ['light', 'dark'] as const) {
+    await page.emulateMedia({ colorScheme })
+    for (const selector of ['.message__avatar.agent-avatar', '.desktop-sidebar .sidebar-item__icon--avatar', '.desktop-sidebar .agent-avatar']) {
+      await expect(page.locator(selector).first()).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
+      await expect(page.locator(selector).first()).toHaveCSS('background-image', 'none')
+    }
+  }
+  await page.emulateMedia({ colorScheme: 'light' })
+
   await expect(page.locator('.message--assistant a').first()).toHaveAttribute(
     'href',
     /\/api\/app\/files\/.+\/download/,
@@ -42,7 +78,7 @@ test('created roles and editable teams share a durable chat list without plugin
   const directURL = page.url()
   await createAgent(page, '开发工程师')
   await expect(page.locator('.message')).toHaveCount(0)
-  await page.getByRole('button', { name: '+ 创建群聊', exact: true }).click()
+  await openCreate(page, '新建群聊')
   const dialog = page.getByRole('dialog')
   await dialog.getByRole('textbox', { name: '名称', exact: true }).fill('产品开发团队')
   await dialog.getByRole('checkbox', { name: '产品经理', exact: true }).check()
@@ -107,28 +143,37 @@ test('created roles and editable teams share a durable chat list without plugin
   await page.getByRole('button', { name: '发送消息', exact: true }).click()
   await expect(page.locator('.message--assistant')).toHaveCount(6)
   await expect(page.locator('.message--assistant').nth(4)).toContainText('我是新成员')
-  await page.getByRole('button',{name:'文件库',exact:true}).click()
+  await page.goto('/files')
   await page.getByRole('button',{name:'查看来源',exact:true}).first().click()
   await expect(page).toHaveURL(groupURL)
   await page.getByRole('button', { name: '归档聊天', exact: true }).click()
   await expect(page.locator('.desktop-sidebar .sidebar-item')).toHaveCount(3)
-  await page.getByRole('checkbox', { name: '显示已归档', exact: true }).first().check()
-  await expect(page.locator('.desktop-sidebar .sidebar-item')).toHaveCount(1)
+  await page.locator('.desktop-sidebar .sidebar-search-trigger').click()
+  const search = page.getByRole('dialog', { name: '搜索聊天', exact: true })
+  await expect(search).toBeVisible()
+  await search.getByRole('searchbox', { name: '搜索聊天', exact: true }).fill('产品开发')
+  await expect(search.getByRole('option')).toHaveCount(0)
+  await search.getByRole('tab', { name: '已归档', exact: true }).click()
+  await expect(search.getByRole('searchbox')).toHaveValue('产品开发')
+  await expect(search.getByRole('option')).toHaveCount(1)
+  await page.screenshot({ path: testInfo.outputPath('archived-chat-search.png'), fullPage: true })
+  await search.getByRole('option').click()
+  await expect(search).not.toBeVisible()
   await page.getByRole('button', { name: '恢复聊天', exact: true }).click()
   await page.goto(directURL)
   await expect(page.locator('.message--assistant')).toHaveCount(1)
   await expect(page.locator('.message--assistant')).toContainText('我是产品经理')
-  await page.getByRole('button', { name: '原生对话', exact: true }).click()
+  await page.goto('/chat')
   await expect(page).toHaveURL(/\/chat/)
   expect(pluginCalls).toEqual([])
   await second.close()
   await page.goto('/conversations')
-  await page.getByRole('button', { name: '+ 创建群聊', exact: true }).click()
+  await openCreate(page, '新建群聊')
   await expect(dialog.getByRole('button', { name: '信息收集团队', exact: true })).toBeDisabled()
   await dialog.getByRole('button', { name: '取消', exact: true }).click()
   await createAgent(page, '模板成员四')
   await createAgent(page, '模板成员五')
-  await page.getByRole('button', { name: '+ 创建群聊', exact: true }).click()
+  await openCreate(page, '新建群聊')
   for (const name of ['信息收集团队', '产品设计团队', '软件开发团队', '文案书写团队', '项目管理团队', '数据分析团队', '会议协作团队', '运维保障团队']) {
     await expect(dialog.getByRole('button', { name, exact: true })).toBeEnabled()
   }
@@ -165,5 +210,17 @@ test('created roles and editable teams share a durable chat list without plugin
   expect(detail.conversation.queuedMessageCount).toBe(1)
   await page.request.post('http://127.0.0.1:19120/__release')
   await expect.poll(async () => (await (await page.request.get(`/api/app/conversations/${detail.conversation.id}`)).json()).conversation.activeRunId).toBeUndefined()
+  await page.setViewportSize({ width: 390, height: 844 })
+  await page.getByRole('button', { name: '打开导航', exact: true }).click()
+  const mobile = page.locator('.mobile-drawer')
+  await expect(mobile.locator('.sidebar-feature-nav')).toHaveCount(0)
+  await mobile.getByRole('button', { name: '新建', exact: true }).click()
+  await expect(page.getByRole('menuitem', { name: '新建 Bot', exact: true })).toBeVisible()
+  await page.keyboard.press('Escape')
+  await mobile.getByRole('button', { name: '搜索', exact: true }).click()
+  await expect(page.getByRole('dialog', { name: '搜索聊天' })).toBeVisible()
+  await expect(page.getByRole('tab', { name: '未归档', exact: true })).toBeVisible()
+  await page.screenshot({ path: testInfo.outputPath('mobile-chat-search.png'), fullPage: true })
+  await page.getByRole('button', { name: '关闭搜索', exact: true }).click()
   expect(pluginCalls).toEqual([])
 })

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