Kaynağa Gözat

优化资源搜索与侧边栏展示

LSamien 2 ay önce
ebeveyn
işleme
73dce66310

Dosya farkı çok büyük olduğundan ihmal edildi
+ 17 - 5
src/client/components/app/FloatingResourceSearch.vue


+ 14 - 19
src/client/components/app/ResourceSidebar.vue

@@ -150,7 +150,7 @@ defineExpose({ focusSearch })
             </button>
             <span v-if="!singleLine" class="sidebar-item__icon" :class="{ 'sidebar-item__icon--avatar': !row.item.icon }">
               <TeamAvatar v-if="row.item.avatar !== undefined || row.item.avatarMembers?.length" :name="row.item.title" :avatar="row.item.avatar || ''" :members="row.item.avatarMembers || []" :fallback-key="row.item.id" :size="23" />
-              <AppIcon v-else-if="row.item.icon" :name="row.item.icon" :size="15" />
+              <AppIcon v-else-if="row.item.icon" :name="row.item.icon" :size="row.item.topic ? 14 : 15" />
               <template v-else>{{ row.item.title.slice(0, 1).toUpperCase() }}</template>
               <span v-if="row.item.status" class="presence" :class="`presence--${row.item.status}`" />
             </span>
@@ -186,14 +186,14 @@ defineExpose({ focusSearch })
 .resource-sidebar { display: flex; height: 100%; min-height: 0; flex-direction: column; }
 .sidebar-search { display: flex; align-items: center; gap: 8px; min-height: 35px; margin: 0 11px 10px; padding: 0 10px; border: 1px solid var(--line); border-radius: 10px; color: var(--text-muted); background: var(--surface-soft); }
 .sidebar-search:focus-within { border-color: var(--line-strong); background: var(--surface-raised); box-shadow: 0 0 0 3px var(--focus-ring); }
-.sidebar-search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: var(--text-primary); font-size: 12px; }
+.sidebar-search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: var(--text-primary); font-size: 13px; }
 .sidebar-search input::placeholder { color: var(--text-muted); }
 .search-reveal-enter-active, .search-reveal-leave-active { transition: opacity 120ms ease, transform 120ms ease; }
 .search-reveal-enter-from, .search-reveal-leave-to { opacity: 0; transform: translateY(-3px); }
 .sidebar-list { min-height: 0; flex: 1; overflow: auto; padding: 0 7px 14px; scrollbar-gutter: stable; }
-.sidebar-section-label { display: flex; align-items: center; gap: 8px; min-height: 25px; padding: 7px 8px 3px; color: var(--text-muted); font-size: 9px; font-weight: 560; }
+.sidebar-section-label { display: flex; align-items: center; gap: 8px; min-height: 25px; padding: 7px 8px 3px; color: var(--text-muted); font-size: 10px; font-weight: 560; }
 .sidebar-section-label::after { height: 1px; flex: 1; background: var(--line); content: ''; }
-.sidebar-section-label--pinned { min-height: 28px; padding-top: 9px; color: var(--text-secondary); font-size: 10px; font-weight: 650; }
+.sidebar-section-label--pinned { min-height: 28px; padding-top: 9px; color: var(--text-secondary); font-size: 11px; font-weight: 650; }
 .sidebar-section-label--pinned::after { background: transparent; }
 .sidebar-item { position: relative; display: flex; align-items: center; gap: 7px; width: 100%; min-height: 42px; padding: 4px 7px; border: 0; border-radius: 8px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; transition: background-color 120ms ease, color 120ms ease; }
 .sidebar-item:hover { background: var(--surface-soft); }
@@ -201,43 +201,38 @@ defineExpose({ focusSearch })
 .sidebar-item--single-line { min-height: 31px; padding-block: 1px; }
 .sidebar-item--single-line .sidebar-item__copy { display: block; }
 .sidebar-item--single-line .sidebar-item__row { min-height: 27px; }
-.sidebar-item--single-line .sidebar-item__row strong { font-size: 11.5px; font-weight: 450; }
+.sidebar-item--single-line .sidebar-item__row strong { font-size: 14px; font-weight: 450; }
 .sidebar-item--single-line .sidebar-item__more { top: 1px; }
 .sidebar-item--nested { min-height: 32px; margin-left: 17px; width: calc(100% - 17px); padding-block: 1px; }
 .sidebar-item--nested .sidebar-item__icon { width: 19px; height: 19px; flex-basis: 19px; border-radius: 6px; color: var(--text-muted); }
 .sidebar-item--nested .sidebar-item__row { min-height: 25px; }
-.sidebar-item--nested .sidebar-item__row strong { font-size: 10.5px; font-weight: 500; }
+.sidebar-item--nested .sidebar-item__row strong { font-size: 11.5px; font-weight: 500; }
 .sidebar-item--nested .sidebar-item__row--secondary { display: none; }
 .sidebar-item--nested .sidebar-item__more { display: none; }
-.sidebar-item--topic { min-height: 32px; padding-block: 1px; }
-.sidebar-item--topic .sidebar-item__icon { width: 19px; height: 19px; flex-basis: 19px; border-radius: 6px; color: var(--text-muted); }
-.sidebar-item--topic .sidebar-item__row { min-height: 25px; }
-.sidebar-item--topic .sidebar-item__row strong { font-size: 10.5px; font-weight: 450; }
-.sidebar-item--topic .sidebar-item__row--secondary { display: none; }
-.sidebar-item--topic .sidebar-item__more { display: none; }
+.sidebar-item--topic .sidebar-item__row strong { font-size: 14px; font-weight: 450; }
 .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: 9px; font-weight: 700; }
+.sidebar-item__icon--avatar { background: var(--surface-hover); 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; }
 .sidebar-item__row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
 .sidebar-item__row strong, .sidebar-item__row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.sidebar-item__row strong { display: flex; min-width: 0; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 570; }
+.sidebar-item__row strong { display: flex; min-width: 0; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 570; }
 .sidebar-item__row strong :deep(.app-icon) { color: var(--text-muted); }
-.sidebar-item__row small { flex: 0 0 auto; color: var(--text-muted); font-size: 8.5px; font-variant-numeric: tabular-nums; }
-.sidebar-item__row--secondary { color: var(--text-muted); font-size: 9.5px; line-height: 13px; }
-.sidebar-item__row b { display: grid; place-items: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--accent); color: var(--text-on-solid); font-size: 8px; }
+.sidebar-item__row small { flex: 0 0 auto; color: var(--text-muted); font-size: 9.5px; font-variant-numeric: tabular-nums; }
+.sidebar-item__row--secondary { color: var(--text-muted); font-size: 10.5px; line-height: 13px; }
+.sidebar-item__row b { display: grid; place-items: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--accent); color: var(--text-on-solid); font-size: 9px; }
 .sidebar-item__more { display: none; position: absolute; right: 5px; top: 7px; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 7px; background: var(--surface-raised); color: var(--text-secondary); cursor: pointer; box-shadow: 0 1px 5px rgba(0,0,0,.07); }
 .sidebar-item:hover .sidebar-item__more, .sidebar-item:focus-within .sidebar-item__more { display: grid; }
-.sidebar-load-more { display: block; width: calc(100% - 14px); min-height: 32px; margin: 8px 7px 2px; border: 0; border-radius: 8px; background: var(--surface-soft); color: var(--text-secondary); cursor: pointer; font: 10px var(--font-ui); }
+.sidebar-load-more { display: block; width: calc(100% - 14px); min-height: 32px; margin: 8px 7px 2px; border: 0; border-radius: 8px; background: var(--surface-soft); color: var(--text-secondary); cursor: pointer; font: 11px var(--font-ui); }
 .sidebar-load-more:hover:not(:disabled) { background: var(--surface-hover); color: var(--text-primary); }.sidebar-load-more:disabled { cursor: progress; opacity: .7; }
 .sidebar-loading { display: flex; flex-direction: column; gap: 4px; padding: 2px; }
 .sidebar-skeleton { height: 38px; border-radius: 8px; background: linear-gradient(90deg, var(--surface-soft), var(--surface-hover), var(--surface-soft)); background-size: 200% 100%; animation: shimmer 1.5s linear infinite; }
 @keyframes shimmer { to { background-position: -200% 0; } }
 .sidebar-empty { display: grid; place-items: center; padding: 45px 22px; color: var(--text-muted); text-align: center; }
 .sidebar-empty > span { display: grid; place-items: center; width: 38px; height: 38px; margin-bottom: 10px; border-radius: 12px; background: var(--surface-soft); }
-.sidebar-empty strong { color: var(--text-secondary); font-size: 12px; }.sidebar-empty p { margin: 5px 0 0; font-size: 10px; line-height: 1.5; }
+.sidebar-empty strong { color: var(--text-secondary); font-size: 13px; }.sidebar-empty p { margin: 5px 0 0; font-size: 11px; line-height: 1.5; }
 @media (prefers-reduced-motion: reduce) { .sidebar-item, .search-reveal-enter-active, .search-reveal-leave-active { transition: none; } }
 </style>

+ 17 - 22
src/client/components/app/WorkspaceShell.vue

@@ -121,7 +121,7 @@ const activeNav = computed(() => {
 
 const contextHeading = computed(() => ({
   chat: '历史记录',
-  groups: '团队列表',
+  groups: '话题列表',
   files: '历史记录',
 })[activeNav.value.key])
 
@@ -368,7 +368,6 @@ onBeforeUnmount(() => {
           </button>
           <button class="sidebar-settings-trigger" type="button" title="设置中心" aria-label="打开设置中心" @click="openSettings('agent-identity', $event)">
             <AppIcon name="settings" :size="17" />
-            <span>设置</span>
           </button>
           <Transition name="menu-fade">
             <div v-if="profileMenuOpen" class="profile-menu" role="listbox" aria-label="切换 Agent" @keydown="handleProfileMenuKeydown">
@@ -475,7 +474,6 @@ onBeforeUnmount(() => {
           </button>
           <button class="sidebar-settings-trigger" type="button" title="设置中心" aria-label="打开设置中心" @click="openSettings('agent-identity', $event)">
             <AppIcon name="settings" :size="17" />
-            <span>设置</span>
           </button>
           <Transition name="menu-fade">
             <div v-if="profileMenuOpen" class="profile-menu" role="listbox" aria-label="切换 Agent" @keydown="handleProfileMenuKeydown">
@@ -603,8 +601,8 @@ onBeforeUnmount(() => {
   color: var(--text-primary);
   cursor: pointer;
   text-align: left;
-  font-size: 13px;
-  font-weight: 580;
+  font-size: 14px;
+  font-weight: 650;
 }
 .sidebar-search-trigger:hover,
 .sidebar-search-trigger[aria-expanded="true"],
@@ -642,8 +640,8 @@ onBeforeUnmount(() => {
   background: transparent;
   color: var(--text-primary);
   cursor: pointer;
-  font-size: 13px;
-  font-weight: 580;
+  font-size: 14px;
+  font-weight: 650;
 }
 .sidebar-primary-action :deep(button:hover),
 .sidebar-primary-action :deep(button:focus-visible) { background: var(--surface-soft); }
@@ -657,21 +655,19 @@ onBeforeUnmount(() => {
 
 .sidebar-context { display: flex; min-height: 0; flex: 1; flex-direction: column; overflow: hidden; }
 .sidebar-context__heading { display: flex; min-height: 35px; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 20px 5px 21px; }
-.sidebar-context__heading strong { font-size: 12px; font-weight: 680; }
-.sidebar-context__heading span { overflow: hidden; color: var(--text-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
+.sidebar-context__heading strong { font-size: 13px; font-weight: 680; }
+.sidebar-context__heading span { overflow: hidden; color: var(--text-muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
 .sidebar-context__body { min-height: 0; flex: 1; overflow: hidden; }
 .sidebar-context:not(.sidebar-context--searching) :deep(.library-search) { display: none; }
 .sidebar-context__body :deep(.library-search) { margin-bottom: 10px; }
 .sidebar-context__body :deep(.sidebar-list) { padding-inline: 10px; }
 .sidebar-context__body :deep(.sidebar-item) { min-height: 42px; padding: 5px 9px; }
 .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: 11.5px; font-weight: 450; }
+.sidebar-context__body :deep(.sidebar-item--single-line .sidebar-item__row strong) { font-size: 14px; font-weight: 450; }
 .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__row strong) { font-size: 12px; }
-.sidebar-context__body :deep(.sidebar-item.sidebar-item--topic) { min-height: 28px; padding-block: 0; }
-.sidebar-context__body :deep(.sidebar-item.sidebar-item--topic .sidebar-item__icon) { width: 17px; height: 17px; flex-basis: 17px; }
-.sidebar-context__body :deep(.sidebar-item.sidebar-item--topic .sidebar-item__row strong) { font-size: 10.5px; font-weight: 450; }
+.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; }
 .sidebar-context__body :deep(.library-sidebar section) { margin-top: 2px; }
 
@@ -687,21 +683,21 @@ onBeforeUnmount(() => {
 .sidebar-account-switcher { position: relative; z-index: 30; display: flex; min-height: 46px; align-items: center; gap: 6px; padding: 4px 2px; border-top: 1px solid var(--line); }
 .sidebar-account-switcher__main { display: flex; min-width: 0; min-height: 38px; flex: 1; align-items: center; gap: 9px; padding: 4px 1px; border: 0; border-radius: 8px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
 .sidebar-account-switcher__main:hover { background: var(--surface-soft); }
-.sidebar-settings-trigger { display: flex; min-width: 58px; height: 38px; flex: 0 0 auto; align-items: center; justify-content: center; gap: 5px; padding: 0 8px; border: 0; border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; font: 600 11px var(--font-ui); }
+.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: 12px; font-weight: 700; }
+.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-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: 10px; font-weight: 650; }
+.profile-menu__heading { display: block; padding: 8px 8px 5px; color: var(--text-muted); font-size: 11px; font-weight: 650; }
 .profile-menu button { display: flex; width: 100%; min-height: 38px; align-items: center; gap: 9px; padding: 5px 8px; border: 0; border-radius: 8px; background: transparent; cursor: pointer; text-align: left; }
 .profile-menu button:hover, .profile-menu button.active { background: var(--surface-hover); }
-.profile-menu button > span:not(.agent-avatar) { display: grid; width: 25px; height: 25px; place-items: center; border-radius: 8px; background: var(--surface-soft); color: var(--text-secondary); font-size: 10px; }
-.profile-menu button strong { flex: 1; overflow: hidden; font-size: 12px; text-overflow: ellipsis; }
+.profile-menu button > span:not(.agent-avatar) { display: grid; width: 25px; height: 25px; place-items: center; border-radius: 8px; background: var(--surface-soft); color: var(--text-secondary); font-size: 11px; }
+.profile-menu button strong { flex: 1; overflow: hidden; font-size: 13px; text-overflow: ellipsis; }
 .account-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }
 .account-copy strong, .account-copy span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.account-copy strong { font-size: 12px; font-weight: 650; }
-.account-copy span { color: var(--text-muted); font-size: 10px; }
+.account-copy strong { font-size: 13px; font-weight: 650; }
+.account-copy span { color: var(--text-muted); font-size: 11px; }
 
 .desktop-sidebar--collapsed .sidebar-brand-row { padding-inline: 21px; }
 .desktop-sidebar--collapsed .sidebar-brand { width: 26px; flex: 0 0 26px; overflow: hidden; }
@@ -721,7 +717,6 @@ onBeforeUnmount(() => {
 .desktop-sidebar--collapsed .sidebar-account-switcher { width: 42px; flex-direction: column; gap: 3px; padding: 0; border-top: 0; }
 .desktop-sidebar--collapsed .sidebar-account-switcher__main { width: 42px; min-height: 42px; flex: 0 0 42px; justify-content: center; padding: 0; }
 .desktop-sidebar--collapsed .sidebar-settings-trigger { width: 42px; min-width: 42px; height: 42px; padding: 0; }
-.desktop-sidebar--collapsed .sidebar-settings-trigger span { display: none; }
 .desktop-sidebar--collapsed .profile-menu { right: auto; bottom: 0; left: calc(100% + 8px); width: 220px; }
 
 .workspace-main { position: relative; display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; background: var(--canvas); }

+ 1 - 1
src/client/components/app/types.ts

@@ -13,7 +13,7 @@ export type SidebarItem = {
   expandable?: boolean
   expanded?: boolean
   status?: 'online' | 'working' | 'offline'
-  icon?: 'chat' | 'groups' | 'file' | 'image' | 'video' | 'audio' | 'link' | 'artifacts' | 'branch' | 'topic' | 'chevron-left' | 'plus'
+  icon?: 'chat' | 'groups' | 'file' | 'image' | 'video' | 'audio' | 'link' | 'artifacts' | 'branch' | 'topic' | 'chevron-left' | 'plus' | 'archive'
   avatar?: string
   avatarMembers?: Array<{ name: string; avatar?: string }>
 }

+ 63 - 0
src/client/components/groups/TopicTeamPickerDialog.vue

@@ -0,0 +1,63 @@
+<script setup lang="ts">
+import { onBeforeUnmount, onMounted } from 'vue'
+import type { GroupRoomSummary } from '@shared/types'
+import AppIcon from '@/components/common/AppIcon.vue'
+import TeamAvatar from '@/components/common/TeamAvatar.vue'
+
+const props = withDefaults(defineProps<{
+  open: boolean
+  rooms: GroupRoomSummary[]
+  currentRoomId?: string
+}>(), { currentRoomId: '' })
+
+const emit = defineEmits<{ close: []; select: [roomId: string] }>()
+
+function close() { emit('close') }
+function onKeydown(event: KeyboardEvent) {
+  if (props.open && event.key === 'Escape') close()
+}
+
+onMounted(() => document.addEventListener('keydown', onKeydown))
+onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
+</script>
+
+<template>
+  <Teleport to="body">
+    <Transition name="topic-team-picker-fade">
+      <div v-if="open" class="topic-team-picker-layer" role="presentation" @mousedown.self="close">
+        <section class="topic-team-picker" role="dialog" aria-modal="true" aria-labelledby="topic-team-picker-title">
+          <header>
+            <div><small>新建话题</small><h2 id="topic-team-picker-title">选择团队</h2></div>
+            <button class="icon-button" type="button" aria-label="关闭" @click="close"><AppIcon name="close" :size="17" /></button>
+          </header>
+          <p class="topic-team-picker__hint">话题将创建在所选团队中。</p>
+          <div class="topic-team-picker__list">
+            <button v-for="room in rooms" :key="room.id" type="button" :class="{ current: room.id === currentRoomId }" @click="emit('select', room.id)">
+              <TeamAvatar :name="room.name" :avatar="room.avatar || ''" :members="room.avatarMembers?.map(member => ({ name: member.displayName })) || []" :fallback-key="room.id" :size="34" />
+              <span><strong>{{ room.name }}</strong><small>{{ room.agentCount }} 个 Agent</small></span>
+              <em v-if="room.id === currentRoomId">当前</em>
+              <AppIcon class="topic-team-picker__next" name="chevron-left" :size="15" />
+            </button>
+          </div>
+          <p v-if="!rooms.length" class="topic-team-picker__empty">当前没有可用团队,请先新建团队。</p>
+        </section>
+      </div>
+    </Transition>
+  </Teleport>
+</template>
+
+<style scoped>
+.topic-team-picker-layer { position: fixed; z-index: 230; inset: 0; display: grid; place-items: center; padding: 16px; background: var(--scrim); backdrop-filter: blur(4px); }
+.topic-team-picker { display: flex; width: min(420px, calc(100vw - 32px)); max-height: min(600px, calc(100dvh - 32px)); padding: 14px; overflow: hidden; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-raised); box-shadow: var(--shadow-float); flex-direction: column; }
+header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 2px 2px 10px 7px; } header small { color: var(--text-muted); font-size: 10px; letter-spacing: .04em; } h2 { margin: 3px 0 0; color: var(--text-primary); font-size: 18px; font-weight: 680; letter-spacing: -.025em; }
+.icon-button { display: grid; width: 32px; height: 32px; flex: 0 0 32px; place-items: center; padding: 0; border: 0; border-radius: 9px; background: transparent; color: var(--text-muted); cursor: pointer; }.icon-button:hover, .icon-button:focus-visible { outline: 0; background: var(--surface-soft); color: var(--text-primary); }
+.topic-team-picker__hint { margin: 0 7px 10px; color: var(--text-muted); font-size: 11px; }
+.topic-team-picker__list { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
+.topic-team-picker__list > button { display: flex; width: 100%; min-height: 52px; align-items: center; gap: 10px; padding: 7px 9px; border: 0; border-radius: 10px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }.topic-team-picker__list > button:hover, .topic-team-picker__list > button:focus-visible { outline: 0; background: var(--surface-soft); }.topic-team-picker__list > button.current { background: color-mix(in srgb, var(--accent) 7%, transparent); }
+.topic-team-picker__list > button > span { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }.topic-team-picker__list strong, .topic-team-picker__list small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.topic-team-picker__list strong { font-size: 14px; font-weight: 620; }.topic-team-picker__list small { color: var(--text-muted); font-size: 10px; font-weight: 400; }.topic-team-picker__list em { flex: 0 0 auto; padding: 2px 6px; border-radius: 999px; background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); font-size: 9px; font-style: normal; font-weight: 620; }.topic-team-picker__list :deep(.app-icon) { flex: 0 0 auto; color: var(--text-muted); }
+.topic-team-picker__next { transform: rotate(180deg); }
+.topic-team-picker__empty { margin: 0; padding: 34px 12px; color: var(--text-muted); font-size: 12px; text-align: center; }
+.topic-team-picker-fade-enter-active, .topic-team-picker-fade-leave-active { transition: opacity 130ms ease; }.topic-team-picker-fade-enter-active .topic-team-picker, .topic-team-picker-fade-leave-active .topic-team-picker { transition: transform 160ms var(--ease-out); }.topic-team-picker-fade-enter-from, .topic-team-picker-fade-leave-to { opacity: 0; }.topic-team-picker-fade-enter-from .topic-team-picker, .topic-team-picker-fade-leave-to .topic-team-picker { transform: translateY(7px) scale(.99); }
+@media (max-width: 600px) { .topic-team-picker-layer { padding: 12px; }.topic-team-picker { width: min(420px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); } }
+@media (prefers-reduced-motion: reduce) { .topic-team-picker-fade-enter-active, .topic-team-picker-fade-leave-active, .topic-team-picker-fade-enter-active .topic-team-picker, .topic-team-picker-fade-leave-active .topic-team-picker { transition: none; } }
+</style>

+ 3 - 3
src/client/components/library/LibrarySidebar.vue

@@ -33,7 +33,7 @@ const emit = defineEmits<{ search: [value: string]; filter: [id: string]; refres
 
 <style scoped>
 .library-sidebar { display: flex; height: 100%; min-height: 0; flex-direction: column; padding: 0 11px 13px; }
-.library-search { display: flex; min-height: 35px; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-soft); color: var(--text-muted); }.library-search:focus-within { border-color: var(--line-strong); background: var(--surface-raised); box-shadow: 0 0 0 3px var(--focus-ring); }.library-search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: var(--text-primary); font-size: 12px; }
-section { margin-top: 18px; } h3 { margin: 0 7px 7px; color: var(--text-muted); font-size: 9px; font-weight: 650; letter-spacing: .07em; text-transform: uppercase; } section button { display: flex; width: 100%; min-height: 39px; align-items: center; gap: 9px; padding: 0 9px; border: 0; border-radius: 9px; background: transparent; color: var(--text-secondary); cursor: pointer; text-align: left; } section button:hover, section button.active { background: var(--surface-hover); color: var(--text-primary); } section button.active { font-weight: 610; } section button span { flex: 1; font-size: 11px; } section button small { color: var(--text-muted); font-size: 9px; }
-.library-sync { margin-top: auto; padding: 12px 4px 0; border-top: 1px solid var(--line); }.library-sync p { display: flex; align-items: flex-start; gap: 5px; margin: 0 0 8px; color: var(--text-muted); font-size: 9px; line-height: 1.5; }.library-sync p.error { color: var(--danger); }.library-sync .quiet-button { display: flex; width: 100%; gap: 6px; font-size: 10px; }
+.library-search { display: flex; min-height: 35px; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-soft); color: var(--text-muted); }.library-search:focus-within { border-color: var(--line-strong); background: var(--surface-raised); box-shadow: 0 0 0 3px var(--focus-ring); }.library-search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: var(--text-primary); font-size: 13px; }
+section { margin-top: 18px; } h3 { margin: 0 7px 7px; color: var(--text-muted); font-size: 10px; font-weight: 650; letter-spacing: .07em; text-transform: uppercase; } section button { display: flex; width: 100%; min-height: 39px; align-items: center; gap: 9px; padding: 0 9px; border: 0; border-radius: 9px; background: transparent; color: var(--text-secondary); cursor: pointer; text-align: left; } section button:hover, section button.active { background: var(--surface-hover); color: var(--text-primary); } section button.active { font-weight: 610; } section button span { flex: 1; font-size: 12px; } section button small { color: var(--text-muted); font-size: 10px; }
+.library-sync { margin-top: auto; padding: 12px 4px 0; border-top: 1px solid var(--line); }.library-sync p { display: flex; align-items: flex-start; gap: 5px; margin: 0 0 8px; color: var(--text-muted); font-size: 10px; line-height: 1.5; }.library-sync p.error { color: var(--danger); }.library-sync .quiet-button { display: flex; width: 100%; gap: 6px; font-size: 11px; }
 </style>

+ 19 - 15
src/client/components/messages/MessageTimeline.vue

@@ -396,36 +396,40 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
 .timeline { min-height: 0; flex: 1; overflow-y: auto; overscroll-behavior: contain; background: var(--conversation-canvas); }
 .message-stack { width: min(780px, 100%); min-height: 100%; margin: 0 auto; padding: 24px 24px 36px; }
 .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: 9px; }.load-older:hover { background: var(--surface-soft); color: var(--text-secondary); }
+.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: 10px; font-weight: 700; }
+.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__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: 9px; }.message__meta strong { display: inline-flex; align-items: center; color: var(--text-primary); font-size: 11px; 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__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); }
+.message__content :deep(.plain-text), .message__content :deep(.markdown) { font-size: 15px; }
+.message__content :deep(.code-lang), .message__content :deep(.code-copy) { font-size: 10px; }
+.message__content :deep(.code-block code) { font-size: 12px; }
+.message__content :deep(.file-link-card)::after { font-size: 10px; }
 .message--user { justify-content: flex-end; margin-top: 27px; }
 .message--user .message__body { max-width: min(610px, 76%); padding: 11px 15px; border-radius: 20px 20px 6px 20px; background: #eceff3; }
 .dark .message--user .message__body { background: #2d323a; }
 .message--user .message__meta { display: none; }
-.message__delivery { display: flex; min-height: 18px; align-items: center; justify-content: flex-end; gap: 4px; margin: 5px 1px -4px; color: var(--text-muted); font-size: 9px; }.message__delivery--accepted { color: var(--success); }.message__delivery--failed, .message__delivery--unknown-receipt { color: var(--danger); }
-.thinking-indicator { display: flex; min-height: 34px; align-items: center; margin: 5px 0 20px; color: var(--text-muted); }.thinking-indicator > span { display: inline-flex; align-items: baseline; gap: 7px; }.thinking-indicator strong { color: var(--text-secondary); font-size: 11px; font-weight: 600; background: linear-gradient(90deg, var(--text-muted), var(--text-primary), var(--text-muted)); background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: thinking-shimmer 2.2s linear infinite; }.thinking-indicator small { font-size: 9px; font-variant-numeric: tabular-nums; }
+.message__delivery { display: flex; min-height: 18px; align-items: center; justify-content: flex-end; gap: 4px; margin: 5px 1px -4px; color: var(--text-muted); font-size: 10px; }.message__delivery--accepted { color: var(--success); }.message__delivery--failed, .message__delivery--unknown-receipt { color: var(--danger); }
+.thinking-indicator { display: flex; min-height: 34px; align-items: center; margin: 5px 0 20px; color: var(--text-muted); }.thinking-indicator > span { display: inline-flex; align-items: baseline; gap: 7px; }.thinking-indicator strong { color: var(--text-secondary); font-size: 12px; font-weight: 600; background: linear-gradient(90deg, var(--text-muted), var(--text-primary), var(--text-muted)); background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: thinking-shimmer 2.2s linear infinite; }.thinking-indicator small { font-size: 10px; font-variant-numeric: tabular-nums; }
 @keyframes thinking-shimmer { to { background-position: -200% 0; } }
 .message--system { justify-content: center; }.message--system .message__avatar, .message--system .message__meta { display: none; }.message--system .message__body { max-width: 82%; padding: 6px 10px; border: 1px solid var(--line); border-radius: 9px; color: var(--text-muted); text-align: center; }
-.delegation-event { min-width: 230px; max-width: 460px; text-align: left; }.delegation-event summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; }.delegation-event summary::-webkit-details-marker { display: none; }.delegation-event summary > span { display: flex; min-width: 0; flex-direction: column; gap: 2px; }.delegation-event strong { color: var(--text-secondary); font-size: 11px; font-weight: 650; }.delegation-event small { color: var(--text-muted); font-size: 9px; }.delegation-event :deep(.markdown) { max-height: 260px; margin-top: 9px; overflow: auto; color: var(--text-secondary); font-size: 10px; }
-.system-event { max-width: 460px; text-align: left; }.system-event summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; color: var(--text-muted); font-size: 10px; }.system-event summary::-webkit-details-marker { display: none; }.system-event :deep(.markdown) { max-height: 220px; margin-top: 8px; overflow: auto; color: var(--text-secondary); font-size: 10px; }
+.delegation-event { min-width: 230px; max-width: 460px; text-align: left; }.delegation-event summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; }.delegation-event summary::-webkit-details-marker { display: none; }.delegation-event summary > span { display: flex; min-width: 0; flex-direction: column; gap: 2px; }.delegation-event strong { color: var(--text-secondary); font-size: 12px; font-weight: 650; }.delegation-event small { color: var(--text-muted); font-size: 10px; }.delegation-event :deep(.markdown) { max-height: 260px; margin-top: 9px; overflow: auto; color: var(--text-secondary); font-size: 11px; }
+.system-event { max-width: 460px; text-align: left; }.system-event summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; color: var(--text-muted); font-size: 11px; }.system-event summary::-webkit-details-marker { display: none; }.system-event :deep(.markdown) { max-height: 220px; margin-top: 8px; overflow: auto; color: var(--text-secondary); font-size: 11px; }
 .message--tool-only { margin-block: 4px; }.message--tool-only .message__avatar, .message--tool-only .message__meta, .message--tool-only .message__actions { display: none; }.message--tool-only .message__body { max-width: 100%; }
 .message--assistant-internal { margin-bottom: 6px; }.message--assistant-internal .message__reasoning { margin-bottom: 4px; padding-bottom: 4px; }.message--assistant-internal .message__tools { margin-top: 4px; }
 .message--assistant-anonymous .message__body { max-width: min(680px, 100%); }
-.message__reasoning { max-width: 420px; margin: 3px 0 10px; padding: 0 0 8px; border: 0; border-bottom: 1px dashed var(--line-strong); color: var(--text-secondary); font-size: 11px; }.message__reasoning summary { display: flex; align-items: center; gap: 5px; list-style: none; color: var(--text-muted); cursor: pointer; font-size: 9px; }.message__reasoning summary::-webkit-details-marker { display: none; }.message__reasoning summary::before { content: '›'; color: var(--text-muted); font-size: 14px; line-height: 1; transition: transform 120ms ease; }.message__reasoning[open] summary::before { transform: rotate(90deg); }.message__reasoning :deep(.markdown) { margin-top: 8px; }
-.fork-divider { display: flex; align-items: center; gap: 12px; margin: 8px 0 28px; color: var(--text-muted); font-size: 10px; }.fork-divider::before, .fork-divider::after { height: 1px; flex: 1; background: var(--line); content: ''; }.fork-divider > span { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface-raised); white-space: nowrap; }.fork-divider strong { max-width: 180px; overflow: hidden; color: var(--text-secondary); text-overflow: ellipsis; }
-.message__attachments { display: flex; margin-top: 8px; flex-wrap: wrap; gap: 7px; }.message__attachments button { display: flex; min-width: 90px; max-width: 210px; min-height: 42px; align-items: center; gap: 7px; padding: 5px 8px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); color: var(--text-secondary); cursor: pointer; }.message__attachments button:hover { border-color: var(--line-strong); }.message__attachments img { width: 48px; height: 48px; margin: -5px 0 -5px -8px; object-fit: cover; }.message__attachments button > span { display: grid; place-items: center; width: 25px; height: 25px; border-radius: 7px; background: var(--surface-soft); }.message__attachments strong { min-width: 0; overflow: hidden; font-size: 10px; font-weight: 520; text-overflow: ellipsis; white-space: nowrap; }
-.message--user .message__attachments { margin: 0 0 9px; }.message--user .message__attachments button { width: 100%; max-width: none; min-height: 50px; padding: 7px 9px; border: 0; border-radius: 11px; background: rgba(255,255,255,.48); color: var(--text-primary); }.message--user .message__attachments button > span { width: 28px; height: 28px; background: rgba(255,255,255,.65); }.message--user .message__attachments strong { font-size: 11px; font-weight: 560; }.dark .message--user .message__attachments button { background: rgba(255,255,255,.08); }.message--user .message__attachments button.message__attachment--image { display: block; width: auto; min-width: 0; max-width: min(360px, 100%); min-height: 0; padding: 0; border: 0; border-radius: 8px; background: transparent; }.message--user .message__attachments button.message__attachment--image img { display: block; width: auto; height: auto; max-width: min(360px, 100%); max-height: 420px; margin: 0; border-radius: inherit; object-fit: contain; }
-.message__tools { margin-top: 9px; }.message__error { display: flex; align-items: center; gap: 5px; margin: 7px 0 0; color: var(--danger); font-size: 9px; }
+.message__reasoning { max-width: 420px; margin: 3px 0 10px; padding: 0 0 8px; border: 0; border-bottom: 1px dashed var(--line-strong); color: var(--text-secondary); font-size: 12px; }.message__reasoning summary { display: flex; align-items: center; gap: 5px; list-style: none; color: var(--text-muted); cursor: pointer; font-size: 10px; }.message__reasoning summary::-webkit-details-marker { display: none; }.message__reasoning summary::before { content: '›'; color: var(--text-muted); font-size: 14px; line-height: 1; transition: transform 120ms ease; }.message__reasoning[open] summary::before { transform: rotate(90deg); }.message__reasoning :deep(.markdown) { margin-top: 8px; }
+.fork-divider { display: flex; align-items: center; gap: 12px; margin: 8px 0 28px; color: var(--text-muted); font-size: 11px; }.fork-divider::before, .fork-divider::after { height: 1px; flex: 1; background: var(--line); content: ''; }.fork-divider > span { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface-raised); white-space: nowrap; }.fork-divider strong { max-width: 180px; overflow: hidden; color: var(--text-secondary); text-overflow: ellipsis; }
+.message__attachments { display: flex; margin-top: 8px; flex-wrap: wrap; gap: 7px; }.message__attachments button { display: flex; min-width: 90px; max-width: 210px; min-height: 42px; align-items: center; gap: 7px; padding: 5px 8px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); color: var(--text-secondary); cursor: pointer; }.message__attachments button:hover { border-color: var(--line-strong); }.message__attachments img { width: 48px; height: 48px; margin: -5px 0 -5px -8px; object-fit: cover; }.message__attachments button > span { display: grid; place-items: center; width: 25px; height: 25px; border-radius: 7px; background: var(--surface-soft); }.message__attachments strong { min-width: 0; overflow: hidden; font-size: 11px; font-weight: 520; text-overflow: ellipsis; white-space: nowrap; }
+.message--user .message__attachments { margin: 0 0 9px; }.message--user .message__attachments button { width: 100%; max-width: none; min-height: 50px; padding: 7px 9px; border: 0; border-radius: 11px; background: rgba(255,255,255,.48); color: var(--text-primary); }.message--user .message__attachments button > span { width: 28px; height: 28px; background: rgba(255,255,255,.65); }.message--user .message__attachments strong { font-size: 12px; font-weight: 560; }.dark .message--user .message__attachments button { background: rgba(255,255,255,.08); }.message--user .message__attachments button.message__attachment--image { display: block; width: auto; min-width: 0; max-width: min(360px, 100%); min-height: 0; padding: 0; border: 0; border-radius: 8px; background: transparent; }.message--user .message__attachments button.message__attachment--image img { display: block; width: auto; height: auto; max-width: min(360px, 100%); max-height: 420px; margin: 0; border-radius: inherit; object-fit: contain; }
+.message__tools { margin-top: 9px; }.message__error { display: flex; align-items: center; gap: 5px; margin: 7px 0 0; color: var(--danger); font-size: 10px; }
 .message__actions { display: flex; position: absolute; left: -3px; top: 100%; gap: 2px; padding-top: 2px; opacity: 0; transition: opacity 120ms ease; }.message:hover .message__actions, .message:focus-within .message__actions { opacity: 1; }.message--user .message__actions { right: 0; left: auto; }.message__actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 7px; background: transparent; color: var(--text-muted); cursor: pointer; }.message__actions button:hover { background: var(--surface-soft); color: var(--text-primary); }.message__actions .message-action--copied { color: var(--success); }.message__actions .message-action--copy-failed { color: var(--danger); }
-.message--assistant-anonymous .message__actions { align-items: center; }.message__action-time { min-width: 36px; padding: 0 4px 0 3px; color: var(--text-muted); font-size: 9px; font-variant-numeric: tabular-nums; }
+.message--assistant-anonymous .message__actions { align-items: center; }.message__action-time { min-width: 36px; padding: 0 4px 0 3px; color: var(--text-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
 .message--failed .message__body { border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); }
 .message--revealed .message__body { animation: reveal-message 1.8s ease; }
 @keyframes reveal-message { 0%, 28% { box-shadow: 0 0 0 5px var(--focus-ring); } 100% { box-shadow: 0 0 0 0 transparent; } }
-.jump-bottom { display: flex; position: absolute; z-index: 8; bottom: 8px; left: 50%; align-items: center; gap: 5px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface-raised); color: var(--text-secondary); cursor: pointer; font-size: 9px; box-shadow: 0 6px 18px rgba(0,0,0,.1); transform: translateX(-50%); }
+.jump-bottom { display: flex; position: absolute; z-index: 8; bottom: 8px; left: 50%; align-items: center; gap: 5px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface-raised); color: var(--text-secondary); cursor: pointer; font-size: 10px; box-shadow: 0 6px 18px rgba(0,0,0,.1); transform: translateX(-50%); }
 .new-chat-empty { display: flex; min-height: 100%; align-items: center; justify-content: center; gap: 18px; padding: 12vh 24px; box-sizing: border-box; flex-direction: column; color: var(--text-muted); text-align: center; }
 .new-chat-empty__logo { display: block; width: min(180px, 34vw); height: auto; opacity: .18; filter: grayscale(1); }
 .new-chat-empty p { margin: 0; color: var(--text-secondary); font-size: 17px; font-weight: 520; letter-spacing: .02em; }
@@ -435,7 +439,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
 @media (max-width: 768px) {
   .timeline-header { min-height: 54px; padding: 8px 12px; }.timeline-state { font-size: 0; }.timeline-state .status-dot { width: 7px; height: 7px; }
   .new-chat-empty { gap: 15px; padding-block: 9vh; }.new-chat-empty__logo { width: min(150px, 42vw); }.new-chat-empty p { font-size: 16px; }
-  .message-stack { padding: 18px 14px 30px; }.message { margin-bottom: 18px; }.message__body { max-width: calc(100% - 37px); }.message--user .message__body { max-width: 88%; }.message__content :deep(.markdown) { font-size: 13px; }
+  .message-stack { padding: 18px 14px 30px; }.message { margin-bottom: 18px; }.message__body { max-width: calc(100% - 37px); }.message--user .message__body { max-width: 88%; }
   .message__actions { opacity: .65; }.message__meta time { display: none; }
 }
 @media (prefers-reduced-motion: reduce) { .thinking-indicator strong { animation: none; } }

+ 91 - 123
src/client/views/GroupsView.vue

@@ -11,6 +11,7 @@ import ComposerShell from '@/components/composer/ComposerShell.vue'
 import type { ComposerOption, ComposerReference, ComposerSubmit } from '@/components/composer/types'
 import CreateGroupDialog from '@/components/groups/CreateGroupDialog.vue'
 import GroupManager from '@/components/groups/GroupManager.vue'
+import TopicTeamPickerDialog from '@/components/groups/TopicTeamPickerDialog.vue'
 import type { GroupProfileOption } from '@/components/groups/types'
 import PreviewModal from '@/components/library/PreviewModal.vue'
 import ImagePreviewLightbox from '@/components/library/ImagePreviewLightbox.vue'
@@ -37,6 +38,7 @@ const groups = useGroupsStore()
 const route = useRoute()
 const router = useRouter()
 const createOpen = ref(false)
+const topicTeamPickerOpen = ref(false)
 const managerOpen = ref(false)
 const showThinking = ref(true)
 const quoted = ref<UiMessage | null>(null)
@@ -54,7 +56,6 @@ const creatingRoom = ref(false)
 const roomActionMenu = ref<{ roomId: string; topicId?: string; x: number; y: number } | null>(null)
 const topicActionRenaming = ref(false)
 const topicActionRenameValue = ref('')
-const topicListRoomId = ref<string | null>(null)
 const archivedOverlayOpen = ref(false)
 const archivedRoomList = ref<GroupRoomSummary[]>([])
 const archivedTopicList = ref<GroupTopicSummary[]>([])
@@ -83,82 +84,75 @@ const roomSidebarItems = computed(() => activeRooms.value.map(room => {
     : room.avatarMembers
   return roomSidebarItem({ ...room, avatarMembers: currentMembers }, agentAvatars.value, agentAvatarsByName.value)
 }))
-const pinnedTopics = computed(() => groups.pinnedTopics.filter(topic => !topic.archived).sort((a, b) => b.updatedAt - a.updatedAt || b.id.localeCompare(a.id)))
-const topicListRoom = computed(() => topicListRoomId.value ? activeRooms.value.find(room => room.id === topicListRoomId.value) : undefined)
-const sidebarSubtitle = computed(() => topicListRoom.value
-  ? `${topicListRoom.value.agentCount} 个 Agent`
-  : groups.availability === 'available' ? `${activeRooms.value.length} 个活跃团队` : `9119 团队 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL}`)
-const GROUP_LIST_BACK_ID = 'group-list'
-const NEW_TOPIC_ID = 'new-topic'
+const activeRoomById = computed(() => new Map(activeRooms.value.map(room => [room.id, room])))
+const allTopics = computed(() => {
+  const topics = new Map<string, GroupTopicSummary>()
+  for (const topic of groups.pinnedTopics) {
+    if (!topic.archived && activeRoomById.value.has(topic.roomId)) topics.set(topic.id, topic)
+  }
+  for (const room of activeRooms.value) {
+    for (const topic of groups.topicsForRoom(room.id)) {
+      if (!topic.archived) topics.set(topic.id, topic)
+    }
+  }
+  return [...topics.values()].sort((a, b) => b.updatedAt - a.updatedAt || b.id.localeCompare(a.id))
+})
+const sidebarSubtitle = computed(() => groups.availability === 'available'
+  ? `${allTopics.value.length} 个话题`
+  : `9119 团队 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL}`)
+const SEARCH_ARCHIVED_ID = 'search:archived'
+const SEARCH_ROOM_PREFIX = 'room:'
 function topicSidebarItemId(roomId: string, topicId: string): string { return `topic:${roomId}:${topicId}` }
 function topicFromSidebarItemId(id: string): { roomId: string; topicId: string } | undefined {
   const match = /^topic:([^:]+):([^:]+)$/.exec(id)
   return match ? { roomId: match[1]!, topicId: match[2]! } : undefined
 }
-function topicTimeSection(updatedAt: number): string {
+function sidebarDate(updatedAt: number): string {
   const timestamp = updatedAt < 10_000_000_000 ? updatedAt * 1000 : updatedAt
-  const today = new Date()
-  today.setHours(0, 0, 0, 0)
-  const yesterday = new Date(today)
-  yesterday.setDate(yesterday.getDate() - 1)
-  if (timestamp >= today.getTime()) return '今天'
-  if (timestamp >= yesterday.getTime()) return '昨天'
-  return '更早'
+  return new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric' }).format(new Date(timestamp))
 }
-const sidebarItems = computed<SidebarItem[]>(() => {
-  const room = topicListRoom.value
-  if (!room) return roomSidebarItems.value.map(item => ({ ...item, section: undefined }))
-  const items: SidebarItem[] = [{
-    id: GROUP_LIST_BACK_ID,
-    title: '返回团队列表',
-    icon: 'chevron-left',
-    showMore: false,
-  }, {
-    id: NEW_TOPIC_ID,
-    title: '新建话题',
-    icon: 'plus',
-    showMore: false,
-  }]
-  const roomTopics = groups.topicsForRoom(room.id)
-  items.push(...roomTopics.filter(topic => topic.pinned).map(topic => ({
-    id: topicSidebarItemId(room.id, topic.id),
+function topicSidebarItem(topic: GroupTopicSummary, section?: string): SidebarItem {
+  const room = activeRoomById.value.get(topic.roomId)
+  const roomItem = roomSidebarItems.value.find(item => item.id === topic.roomId)
+  return {
+    id: topicSidebarItemId(topic.roomId, topic.id),
     title: topic.title,
-    subtitle: topic.preview || `${topic.messageCount} 条消息`,
-    meta: topic.unreadCount ? `${topic.unreadCount} 未读` : `${topic.messageCount} 条`,
+    subtitle: `${room?.name || '未知团队'}:${topic.preview || '暂无消息'}`,
+    meta: sidebarDate(topic.updatedAt),
     unread: topic.unreadCount,
-    pinned: true,
-    section: '话题置顶',
-    icon: 'topic' as const,
+    pinned: topic.pinned,
+    section,
     topic: true,
-    showMore: false,
-    active: topic.id === groups.selectedTopicId,
-  })))
-  items.push(...roomTopics.filter(topic => !topic.pinned).map(topic => ({
-    id: topicSidebarItemId(room.id, topic.id),
-    title: topic.title,
-    subtitle: topic.preview || `${topic.messageCount} 条消息`,
-    meta: topic.unreadCount ? `${topic.unreadCount} 未读` : `${topic.messageCount} 条`,
-    unread: topic.unreadCount,
-    section: topicTimeSection(topic.updatedAt),
-    icon: 'topic' as const,
-    topic: true,
-    showMore: false,
-    active: topic.id === groups.selectedTopicId,
-  })))
-  if (room.id === groups.selectedRoomId && groups.selectedTopicId && !groups.selectedTopic) {
-    items.push({
-      id: topicSidebarItemId(room.id, groups.selectedTopicId),
+    avatar: roomItem?.avatar ?? '',
+    avatarMembers: roomItem?.avatarMembers,
+    active: topic.roomId === groups.selectedRoomId && topic.id === groups.selectedTopicId,
+  }
+}
+const sidebarItems = computed<SidebarItem[]>(() => {
+  const pinned = allTopics.value.filter(topic => topic.pinned).map(topic => topicSidebarItem(topic, '话题置顶'))
+  const recent = allTopics.value.filter(topic => !topic.pinned).map(topic => topicSidebarItem(topic, '最近话题'))
+  const items = [...pinned, ...recent]
+  if (groups.selectedRoomId && groups.selectedTopicId && !groups.selectedTopic && !items.some(item => item.id === topicSidebarItemId(groups.selectedRoomId!, groups.selectedTopicId!))) {
+    const room = activeRoomById.value.get(groups.selectedRoomId)
+    const roomItem = roomSidebarItems.value.find(item => item.id === groups.selectedRoomId)
+    items.unshift({
+      id: topicSidebarItemId(groups.selectedRoomId, groups.selectedTopicId),
       title: '新话题',
-      subtitle: '发送第一条消息以创建',
-      section: '今天',
-      icon: 'topic',
+      subtitle: `${room?.name || '未知团队'}:发送第一条消息以创建`,
+      section: '最近话题',
       topic: true,
-      showMore: false,
+      avatar: roomItem?.avatar ?? '',
+      avatarMembers: roomItem?.avatarMembers,
       active: true,
     })
   }
   return items
 })
+const groupSearchItems = computed<SidebarItem[]>(() => [
+  ...roomSidebarItems.value.map(item => ({ ...item, id: `${SEARCH_ROOM_PREFIX}${item.id}`, section: '团队', showMore: false })),
+  { id: SEARCH_ARCHIVED_ID, title: '已归档内容', subtitle: '查看已归档团队和话题', section: '已归档', icon: 'archive' as const, showMore: false },
+  ...allTopics.value.map(topic => topicSidebarItem(topic, '话题')),
+])
 const localAgentIdentities = computed(() => new Map(auth.profiles.map(profile => [profile.name, {
   name: profile.agentName || profile.displayName || profile.name,
 }])))
@@ -293,7 +287,6 @@ function stopActiveTopic() {
 async function selectRoom(id: string) {
   try { await groups.selectRoom(id) }
   catch { return }
-  topicListRoomId.value = id
   await router.push(groupRoute())
   quoted.value = null
 }
@@ -307,36 +300,28 @@ async function selectTopic(id: string) {
 }
 
 async function selectSidebarItem(id: string) {
-  if (id === GROUP_LIST_BACK_ID) {
-    topicListRoomId.value = null
+  const topic = topicFromSidebarItemId(id)
+  if (!topic) return
+  if (topic.roomId !== groups.selectedRoomId) {
+    try { await groups.selectRoom(topic.roomId, topic.topicId) }
+    catch { return }
+    await router.push(groupRoute())
+    quoted.value = null
     return
   }
-  if (id === NEW_TOPIC_ID && topicListRoom.value) {
-    await startTopic(topicListRoom.value.id)
+  await selectTopic(topic.topicId)
+}
+
+async function selectGroupSearchItem(id: string) {
+  if (id === SEARCH_ARCHIVED_ID) {
+    await openArchivedOverlay()
     return
   }
-  const topic = topicFromSidebarItemId(id)
-  if (topic) {
-    if (topic.roomId !== groups.selectedRoomId) {
-      try { await groups.selectRoom(topic.roomId, topic.topicId) }
-      catch { return }
-      topicListRoomId.value = topic.roomId
-      await router.push(groupRoute())
-      quoted.value = null
-      return
-    }
-    await selectTopic(topic.topicId)
+  if (id.startsWith(SEARCH_ROOM_PREFIX)) {
+    await selectRoom(id.slice(SEARCH_ROOM_PREFIX.length))
     return
   }
-  await selectRoom(id)
-}
-
-async function openRoomManager(id: string) {
-  if (topicFromSidebarItemId(id)) return
-  await selectRoom(id)
-  managerOpen.value = true
-  try { await groups.refreshNodes() }
-  catch { /* Keep the current node list when refreshing is unavailable. */ }
+  await selectSidebarItem(id)
 }
 
 async function openSelectedRoomManager() {
@@ -408,7 +393,6 @@ async function startTopicFromRoomAction() {
 async function startTopic(roomId: string) {
   try {
     await groups.selectRoom(roomId)
-    topicListRoomId.value = roomId
     const topicId = await groups.startNewTopic()
     if (!topicId) return
     await router.push(groupRoute(roomId, topicId))
@@ -418,6 +402,11 @@ async function startTopic(roomId: string) {
   } catch { /* store publishes the error */ }
 }
 
+async function chooseTopicTeam(roomId: string) {
+  topicTeamPickerOpen.value = false
+  await startTopic(roomId)
+}
+
 async function createRoom(payload: { name: string; avatar?: string; members: Array<GroupProfileOption & { description?: string }>; autoReply: boolean; replyRounds: number; instructions?: string; hostProfile?: string; orchestrationMode?: 'free' | 'host' }) {
   const hostProfile = payload.hostProfile || payload.members[0]?.id
   createError.value = ''
@@ -666,12 +655,14 @@ onMounted(async () => {
   void loadPushSubscriptions()
   try {
     await groups.start()
+    if (groups.topicProtocol) {
+      await Promise.all(activeRooms.value.map(room => groups.loadRoomTopics(room.id).catch(() => undefined)))
+    }
     const requested = typeof route.params.roomId === 'string' ? route.params.roomId : ''
     const requestedTopic = typeof route.params.topicId === 'string' ? route.params.topicId : undefined
     if (requested) {
       try {
         await groups.selectRoom(requested, requestedTopic)
-        topicListRoomId.value = requested
       }
       catch {
         if (groups.selectedRoomId) await router.replace(groupRoute())
@@ -694,14 +685,10 @@ onBeforeUnmount(() => {
 watch(() => [route.params.roomId, route.params.topicId] as const, async ([roomValue, topicValue]) => {
   const roomId = typeof roomValue === 'string' ? roomValue : ''
   const topicId = typeof topicValue === 'string' ? topicValue : undefined
-  if (!roomId) {
-    topicListRoomId.value = null
-    return
-  }
+  if (!roomId) return
   if (roomId && (roomId !== groups.selectedRoomId || (groups.topicProtocol && topicId && topicId !== groups.selectedTopicId))) {
     try {
       await groups.selectRoom(roomId, topicId)
-      topicListRoomId.value = roomId
     }
     catch { if (groups.selectedRoomId && route.fullPath !== groupRoute()) await router.replace(groupRoute()) }
   }
@@ -714,39 +701,28 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
 </script>
 
 <template>
-  <WorkspaceView sidebar-title="团队" :sidebar-context-title="topicListRoom?.name" :sidebar-subtitle="sidebarSubtitle" :sidebar-focus-mode="!!topicListRoom" :inspector-open="managerOpen && !!room" inspector-close-label="关闭团队管理" @close-inspector="managerOpen = false">
+  <WorkspaceView sidebar-title="话题" :sidebar-subtitle="sidebarSubtitle" :inspector-open="managerOpen && !!room" inspector-close-label="关闭团队管理" @close-inspector="managerOpen = false">
     <template #sidebar-action>
+      <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available' || !groups.topicProtocol || !activeRooms.length" title="新建话题" aria-label="新建话题" @click="topicTeamPickerOpen = true">
+        <AppIcon name="topic" :size="18" />
+        <span>新建话题</span>
+      </button>
       <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" title="新建团队" aria-label="新建团队" @click="openCreateGroup">
         <YaoYaoSidebarIcon name="add" />
         <span>新建团队</span>
       </button>
-      <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" @click="openArchivedOverlay">
-        <AppIcon name="archive" :size="15" />
-        <span>已归档</span>
-      </button>
-    </template>
-    <template #sidebar-before-heading>
-      <section v-if="pinnedTopics.length && !topicListRoom" class="pinned-topic-list" aria-label="话题置顶">
-        <header class="pinned-topic-list__heading"><strong>话题置顶</strong><span>{{ pinnedTopics.length }} 个置顶话题</span></header>
-        <button v-for="topic in pinnedTopics" :key="topic.id" class="pinned-topic-list__item" type="button" :class="{ active: topic.id === groups.selectedTopicId }" @click="selectSidebarItem(topicSidebarItemId(topic.roomId, topic.id))" @contextmenu.prevent.stop="openTopicActions(topic.roomId, topic.id, $event)">
-          <AppIcon name="topic" :size="14" />
-          <span>{{ topic.title }}</span>
-          <small>{{ activeRooms.find(room => room.id === topic.roomId)?.name || topic.preview }}</small>
-        </button>
-      </section>
     </template>
     <template #sidebar>
       <ResourceSidebar
-        :class="{ 'group-topic-focus-sidebar': !!topicListRoom }"
         :items="sidebarItems"
-        :active-id="groups.selectedRoomId"
+        :active-id="groups.selectedRoomId && groups.selectedTopicId ? topicSidebarItemId(groups.selectedRoomId, groups.selectedTopicId) : ''"
         :loading="groups.isLoading"
         external-search
-        search-placeholder="搜索团队"
-        empty-title="还没有团队"
-        empty-description="新建团队,邀请 1–8 个 Agent 一起协作。"
+        search-placeholder="搜索话题"
+        empty-title="还没有话题"
+        empty-description="点击新建话题,选择一个团队开始协作。"
         @select="selectSidebarItem"
-        @more="openRoomManager"
+        @more="openRoomActions"
         @context-menu="openRoomActions"
       />
     </template>
@@ -836,7 +812,8 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
     </template>
   </WorkspaceView>
 
-  <FloatingResourceSearch section="groups" label="搜索团队" :items="roomSidebarItems" @select="selectRoom" />
+  <FloatingResourceSearch section="groups" label="搜索话题、团队或归档" :items="groupSearchItems" @select="selectGroupSearchItem" />
+  <TopicTeamPickerDialog :open="topicTeamPickerOpen" :rooms="activeRooms" :current-room-id="groups.selectedRoomId" @close="topicTeamPickerOpen = false" @select="chooseTopicTeam" />
   <CreateGroupDialog :open="createOpen" :profiles="profiles" :avatar-enabled="groups.roomAvatarProtocol" :host-enabled="groups.hostProtocol" :host-flow-enabled="groups.hostFlowProtocol" :room-instructions-enabled="groups.roomInstructionsProtocol" :error="createError" :busy="groups.isLoading || creatingRoom" @close="createOpen = false" @create="createRoom" />
   <PreviewModal v-if="preview" :item="preview" :items="conversationMediaItems" @close="preview = null" @add-to-composer="addPreviewToComposer" @source="preview = null" />
   <ImagePreviewLightbox v-model="mediaPreviewIndex" :images="lightboxMedia" :can-add="uploadsEnabled" @add="addMediaToComposer" />
@@ -887,15 +864,6 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
 .sidebar-primary-action:hover, .sidebar-primary-action:focus-visible { background: var(--surface-hover); outline: 0; }
 .sidebar-primary-action:focus-visible { box-shadow: inset 0 0 0 1px var(--line-strong); }
 .sidebar-primary-action:disabled { cursor: not-allowed; opacity: .35; }
-.pinned-topic-list { max-height: 154px; overflow: auto; padding: 0 10px 4px; border-bottom: 1px solid var(--line); }
-.pinned-topic-list__heading { display: flex; min-height: 35px; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 11px 5px; }
-.pinned-topic-list__heading strong { font-size: 12px; font-weight: 680; }.pinned-topic-list__heading span { overflow: hidden; color: var(--text-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
-.pinned-topic-list__item { display: flex; width: 100%; min-height: 32px; align-items: center; gap: 7px; padding: 1px 9px; border: 0; border-radius: 8px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }.pinned-topic-list__item:hover, .pinned-topic-list__item.active { background: var(--surface-hover); }.pinned-topic-list__item :deep(.app-icon) { flex: 0 0 auto; color: var(--text-muted); }.pinned-topic-list__item > span { min-width: 0; overflow: hidden; font-size: 10.5px; font-weight: 450; text-overflow: ellipsis; white-space: nowrap; }.pinned-topic-list__item small { margin-left: auto; overflow: hidden; color: var(--text-muted); font-size: 8.5px; text-overflow: ellipsis; white-space: nowrap; }
-.group-topic-focus-sidebar :deep(.sidebar-list) { padding: 0 0 14px; }
-.group-topic-focus-sidebar :deep(.sidebar-section-label) { margin-inline: 10px; }
-.group-topic-focus-sidebar :deep(.sidebar-item:not([data-sidebar-id="group-list"])) { width: calc(100% - 20px); margin-inline: 10px; }
-.group-topic-focus-sidebar :deep([data-sidebar-id="group-list"]) { position: sticky; z-index: 3; top: 0; min-height: 58px; padding-inline: 17px; border-bottom: 1px solid var(--line); border-radius: 0; background: var(--surface); }
-.group-topic-focus-sidebar :deep([data-sidebar-id="group-list"]:hover), .group-topic-focus-sidebar :deep([data-sidebar-id="group-list"]:focus-visible) { background: var(--surface-hover); }
 .archived-overlay-backdrop { position: fixed; z-index: 220; inset: 0; display: grid; place-items: center; padding: 20px; background: rgba(0,0,0,.36); }.archived-overlay { width: min(460px, 100%); max-height: min(620px, calc(100vh - 40px)); overflow: auto; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-raised); box-shadow: 0 24px 70px rgba(0,0,0,.28); }.archived-overlay header { display: flex; align-items: center; justify-content: space-between; padding: 18px 18px 14px; border-bottom: 1px solid var(--line); }.archived-overlay header small { display: block; color: var(--text-secondary); font-size: 10px; }.archived-overlay header strong { font-size: 15px; }.archived-overlay header button { display: grid; width: 30px; height: 30px; place-items: center; border: 0; border-radius: 8px; background: transparent; color: var(--text-secondary); cursor: pointer; }.archived-section { display: grid; gap: 8px; padding: 16px 18px; }.archived-section + .archived-section { border-top: 1px solid var(--line); }.archived-section h3 { margin: 0; font-size: 12px; }.archived-section p { margin: 0; color: var(--text-secondary); font-size: 12px; }.archived-section article { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; }.archived-section article span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.archived-section article button { border: 0; border-radius: 7px; background: var(--surface-hover); color: var(--text-primary); cursor: pointer; padding: 5px 9px; }
 .group-header-actions { display: flex; align-items: center; gap: 8px; }.group-host-chip { max-width: 150px; overflow: hidden; padding: 4px 7px; border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--line)); border-radius: 999px; background: color-mix(in srgb, var(--accent) 8%, transparent); color: var(--accent); font-size: 9px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }.group-push-button.active { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); }.group-avatars { display: flex; align-items: center; }.group-avatars span, .group-avatars em { display: grid; width: 24px; height: 24px; margin-left: -5px; place-items: center; border: 2px solid var(--canvas); border-radius: 8px; font-size: 8px; font-style: normal; font-weight: 650; }.group-avatars span { background: transparent; color: var(--text-secondary); }.group-avatars span:first-child { margin-left: 0; }.group-avatars em { background: var(--surface-hover); color: var(--text-secondary); }
 .group-error { display: flex; width: min(760px, calc(100% - 32px)); margin: 0 auto 4px; align-items: center; gap: 6px; color: var(--danger); font-size: 9px; }

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

@@ -0,0 +1,39 @@
+import { afterEach, describe, expect, it } from 'vitest'
+import { mount } from '@vue/test-utils'
+import FloatingResourceSearch from '../../src/client/components/app/FloatingResourceSearch.vue'
+
+afterEach(() => { document.body.innerHTML = '' })
+
+describe('FloatingResourceSearch', () => {
+  it('groups team, archived, and topic results and emits the selected entry', async () => {
+    const wrapper = mount(FloatingResourceSearch, {
+      props: {
+        section: 'groups',
+        label: '搜索话题、团队或归档',
+        items: [
+          { id: 'room:r1', title: '测试团队', subtitle: '2 个 Agent', section: '团队', avatar: '' },
+          { id: 'search:archived', title: '已归档内容', subtitle: '查看已归档团队和话题', section: '已归档', icon: 'archive' },
+          { id: 'topic:r1:t1', title: '发布检查', subtitle: '测试团队:准备上线', section: '话题', avatar: '', topic: true },
+        ],
+      },
+      attachTo: document.body,
+    })
+
+    document.dispatchEvent(new CustomEvent('hermes-yaoyao:sidebar-search', { detail: { section: 'groups' } }))
+    await wrapper.vm.$nextTick()
+
+    const dialog = document.querySelector('[role="dialog"]')
+    expect(dialog?.textContent).toContain('团队')
+    expect(dialog?.textContent).toContain('已归档')
+    expect(dialog?.textContent).toContain('话题')
+    expect(dialog?.querySelectorAll('.floating-resource-search__section')).toHaveLength(3)
+    expect(dialog?.querySelectorAll('.team-avatar')).toHaveLength(2)
+
+    const archived = [...document.querySelectorAll<HTMLButtonElement>('[role="option"]')]
+      .find(button => button.textContent?.includes('已归档内容'))
+    archived?.click()
+    await wrapper.vm.$nextTick()
+    expect(wrapper.emitted('select')).toEqual([['search:archived']])
+    wrapper.unmount()
+  })
+})

+ 41 - 0
tests/client/topicTeamPickerDialog.test.ts

@@ -0,0 +1,41 @@
+import { afterEach, describe, expect, it } from 'vitest'
+import { mount } from '@vue/test-utils'
+import TopicTeamPickerDialog from '../../src/client/components/groups/TopicTeamPickerDialog.vue'
+
+const rooms = [{
+  id: 'room-a', name: '产品团队', cwd: '', createdAt: 1, updatedAt: 2, archived: false,
+  agentCount: 3, unreadCount: 0, maxReplyRounds: 3,
+}, {
+  id: 'room-b', name: '运维团队', cwd: '', createdAt: 1, updatedAt: 2, archived: false,
+  agentCount: 2, unreadCount: 0, maxReplyRounds: 3,
+}]
+
+afterEach(() => { document.body.innerHTML = '' })
+
+describe('TopicTeamPickerDialog', () => {
+  it('lists teams and emits the selected team before topic creation', async () => {
+    const wrapper = mount(TopicTeamPickerDialog, {
+      props: { open: true, rooms, currentRoomId: 'room-a' },
+      attachTo: document.body,
+    })
+
+    expect(document.querySelector('[role="dialog"]')?.textContent).toContain('选择团队')
+    expect(document.body.textContent).toContain('产品团队')
+    expect(document.body.textContent).toContain('运维团队')
+    expect(document.body.textContent).toContain('当前')
+
+    const buttons = [...document.querySelectorAll<HTMLButtonElement>('.topic-team-picker__list > button')]
+    buttons[1]?.click()
+    await wrapper.vm.$nextTick()
+    expect(wrapper.emitted('select')).toEqual([['room-b']])
+    wrapper.unmount()
+  })
+
+  it('closes on Escape without selecting a team', async () => {
+    const wrapper = mount(TopicTeamPickerDialog, { props: { open: true, rooms }, attachTo: document.body })
+    document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
+    expect(wrapper.emitted('close')).toHaveLength(1)
+    expect(wrapper.emitted('select')).toBeUndefined()
+    wrapper.unmount()
+  })
+})

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

@@ -95,6 +95,8 @@ describe('Workspace shell account controls', () => {
     expect(desktop.find('.profile-menu').exists()).toBe(false)
 
     const settingsTrigger = desktop.get<HTMLButtonElement>('.sidebar-settings-trigger')
+    expect(settingsTrigger.text()).toBe('')
+    expect(settingsTrigger.attributes('aria-label')).toBe('打开设置中心')
     await settingsTrigger.trigger('click')
     const settings = wrapper.get('[data-testid="settings-center"]')
     expect(settings.attributes('data-page')).toBe('agent-identity')

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor