Kaynağa Gözat

群聊话题列表启用侧栏专注模式

LSamien 2 ay önce
ebeveyn
işleme
7fe7179551

+ 14 - 2
src/client/components/app/WorkspaceShell.vue

@@ -29,6 +29,7 @@ const props = withDefaults(defineProps<{
   sidebarTitle?: string
   sidebarTitle?: string
   sidebarSubtitle?: string
   sidebarSubtitle?: string
   sidebarContextTitle?: string
   sidebarContextTitle?: string
+  sidebarFocusMode?: boolean
   inspectorOpen?: boolean
   inspectorOpen?: boolean
   inspectorCloseLabel?: string
   inspectorCloseLabel?: string
 }>(), {
 }>(), {
@@ -41,6 +42,7 @@ const props = withDefaults(defineProps<{
   sidebarTitle: '',
   sidebarTitle: '',
   sidebarSubtitle: '',
   sidebarSubtitle: '',
   sidebarContextTitle: '',
   sidebarContextTitle: '',
+  sidebarFocusMode: false,
   inspectorOpen: false,
   inspectorOpen: false,
   inspectorCloseLabel: '关闭预览',
   inspectorCloseLabel: '关闭预览',
 })
 })
@@ -167,7 +169,7 @@ onBeforeUnmount(() => {
 </script>
 </script>
 
 
 <template>
 <template>
-  <div class="workspace-shell" :class="{ 'workspace-shell--collapsed': sidebarCollapsed }">
+  <div class="workspace-shell" :class="{ 'workspace-shell--collapsed': sidebarCollapsed, 'workspace-shell--sidebar-focused': sidebarFocusMode }">
     <header class="mobile-header">
     <header class="mobile-header">
       <button class="icon-button" type="button" aria-label="打开导航" @click="mobileDrawerOpen = true">
       <button class="icon-button" type="button" aria-label="打开导航" @click="mobileDrawerOpen = true">
         <AppIcon name="menu" :size="20" />
         <AppIcon name="menu" :size="20" />
@@ -299,7 +301,7 @@ onBeforeUnmount(() => {
     </Transition>
     </Transition>
     <aside
     <aside
       class="mobile-drawer"
       class="mobile-drawer"
-      :class="{ 'mobile-drawer--open': mobileDrawerOpen }"
+      :class="{ 'mobile-drawer--open': mobileDrawerOpen, 'mobile-drawer--focused': sidebarFocusMode }"
       aria-label="移动导航"
       aria-label="移动导航"
       :aria-hidden="!mobileDrawerOpen"
       :aria-hidden="!mobileDrawerOpen"
       :inert="!mobileDrawerOpen"
       :inert="!mobileDrawerOpen"
@@ -559,6 +561,12 @@ onBeforeUnmount(() => {
 .sidebar-context__body :deep(.library-sidebar) { padding-top: 1px; }
 .sidebar-context__body :deep(.library-sidebar) { padding-top: 1px; }
 .sidebar-context__body :deep(.library-sidebar section) { margin-top: 2px; }
 .sidebar-context__body :deep(.library-sidebar section) { margin-top: 2px; }
 
 
+.workspace-shell--sidebar-focused .desktop-sidebar > :not(.sidebar-context),
+.workspace-shell--sidebar-focused .desktop-sidebar .sidebar-context__heading { display: none; }
+.workspace-shell--sidebar-focused { grid-template-columns: 264px minmax(0, 1fr) auto; }
+.workspace-shell--sidebar-focused .desktop-sidebar .sidebar-context,
+.workspace-shell--sidebar-focused .desktop-sidebar--collapsed .sidebar-context { display: flex; flex: 1; }
+
 .sidebar-footer { flex: 0 0 auto; padding: 7px 10px 10px; background: var(--surface); }
 .sidebar-footer { flex: 0 0 auto; padding: 7px 10px 10px; background: var(--surface); }
 .sidebar-account-switcher { position: relative; z-index: 30; display: flex; min-height: 46px; align-items: center; gap: 6px; padding: 4px 7px; border-top: 1px solid var(--line); }
 .sidebar-account-switcher { position: relative; z-index: 30; display: flex; min-height: 46px; align-items: center; gap: 6px; padding: 4px 7px; 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 { 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; }
@@ -617,6 +625,10 @@ onBeforeUnmount(() => {
   .mobile-drawer--open { transform: translateX(0); }
   .mobile-drawer--open { transform: translateX(0); }
   .mobile-drawer__header { display: flex; min-height: 49px; align-items: center; justify-content: space-between; padding: 0 14px 5px 12px; }
   .mobile-drawer__header { display: flex; min-height: 49px; align-items: center; justify-content: space-between; padding: 0 14px 5px 12px; }
   .mobile-drawer__context { margin-top: 2px; }
   .mobile-drawer__context { margin-top: 2px; }
+  .mobile-drawer--focused { padding-block: max(9px, env(safe-area-inset-top)) max(9px, env(safe-area-inset-bottom)); }
+  .mobile-drawer--focused > :not(.sidebar-context),
+  .mobile-drawer--focused .sidebar-context__heading { display: none; }
+  .mobile-drawer--focused .mobile-drawer__context { margin-top: 0; flex: 1; }
   .mobile-drawer__context .sidebar-context__body :deep(.resource-sidebar) { min-height: 0; flex: 1; }
   .mobile-drawer__context .sidebar-context__body :deep(.resource-sidebar) { min-height: 0; flex: 1; }
   .mobile-drawer__footer { padding-inline: 10px; }
   .mobile-drawer__footer { padding-inline: 10px; }
   .workspace-inspector { position: fixed; z-index: 45; inset: 52px 0 0 auto; width: min(440px, 100vw); min-width: 0; box-shadow: var(--shadow-float); }
   .workspace-inspector { position: fixed; z-index: 45; inset: 52px 0 0 auto; width: min(440px, 100vw); min-width: 0; box-shadow: var(--shadow-float); }

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

@@ -10,9 +10,10 @@ withDefaults(defineProps<{
   sidebarTitle: string
   sidebarTitle: string
   sidebarSubtitle?: string
   sidebarSubtitle?: string
   sidebarContextTitle?: string
   sidebarContextTitle?: string
+  sidebarFocusMode?: boolean
   inspectorOpen?: boolean
   inspectorOpen?: boolean
   inspectorCloseLabel?: string
   inspectorCloseLabel?: string
-}>(), { sidebarSubtitle: '', sidebarContextTitle: '', inspectorOpen: false, inspectorCloseLabel: '关闭预览' })
+}>(), { sidebarSubtitle: '', sidebarContextTitle: '', sidebarFocusMode: false, inspectorOpen: false, inspectorCloseLabel: '关闭预览' })
 
 
 const emit = defineEmits<{ closeInspector: [] }>()
 const emit = defineEmits<{ closeInspector: [] }>()
 const auth = useAuthStore()
 const auth = useAuthStore()
@@ -67,6 +68,7 @@ onBeforeUnmount(() => window.removeEventListener('focus', refreshOnWindowFocus))
     :sidebar-title="sidebarTitle"
     :sidebar-title="sidebarTitle"
     :sidebar-subtitle="sidebarSubtitle"
     :sidebar-subtitle="sidebarSubtitle"
     :sidebar-context-title="sidebarContextTitle"
     :sidebar-context-title="sidebarContextTitle"
+    :sidebar-focus-mode="sidebarFocusMode"
     :inspector-open="inspectorOpen"
     :inspector-open="inspectorOpen"
     :inspector-close-label="inspectorCloseLabel"
     :inspector-close-label="inspectorCloseLabel"
     @logout="logout"
     @logout="logout"

+ 8 - 3
src/client/views/GroupsView.vue

@@ -84,8 +84,7 @@ const sidebarItems = computed<SidebarItem[]>(() => {
   if (!room) return activeRooms.value.map(item => ({ ...roomSidebarItem(item), section: undefined }))
   if (!room) return activeRooms.value.map(item => ({ ...roomSidebarItem(item), section: undefined }))
   const items: SidebarItem[] = [{
   const items: SidebarItem[] = [{
     id: GROUP_LIST_BACK_ID,
     id: GROUP_LIST_BACK_ID,
-    title: '返回群聊记录',
-    section: room.name,
+    title: '返回房间列表',
     icon: 'chevron-left',
     icon: 'chevron-left',
     showMore: false,
     showMore: false,
   }, {
   }, {
@@ -612,7 +611,7 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
 </script>
 </script>
 
 
 <template>
 <template>
-  <WorkspaceView sidebar-title="群聊" :sidebar-context-title="topicListRoom?.name" :sidebar-subtitle="sidebarSubtitle" :inspector-open="managerOpen && !!room" inspector-close-label="关闭群聊管理" @close-inspector="managerOpen = false">
+  <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">
     <template #sidebar-action>
     <template #sidebar-action>
       <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" title="新建群聊" aria-label="新建群聊" @click="openCreateGroup">
       <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" title="新建群聊" aria-label="新建群聊" @click="openCreateGroup">
         <YaoYaoSidebarIcon name="add" />
         <YaoYaoSidebarIcon name="add" />
@@ -635,6 +634,7 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
     </template>
     </template>
     <template #sidebar>
     <template #sidebar>
       <ResourceSidebar
       <ResourceSidebar
+        :class="{ 'group-topic-focus-sidebar': !!topicListRoom }"
         :items="sidebarItems"
         :items="sidebarItems"
         :active-id="groups.selectedRoomId"
         :active-id="groups.selectedRoomId"
         :loading="groups.isLoading"
         :loading="groups.isLoading"
@@ -786,6 +786,11 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
 .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 { 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__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; }
 .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; }
 .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-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-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-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; }
 .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; }

+ 10 - 1
tests/e2e/workspace.spec.ts

@@ -545,6 +545,13 @@ test('selects existing group topics and creates a new protocol v4 topic on first
   const roomItem = sidebar.locator(`[data-sidebar-id="${roomId}"]`)
   const roomItem = sidebar.locator(`[data-sidebar-id="${roomId}"]`)
   const existingTopic = sidebar.locator(`[data-sidebar-id="${existingTopicSidebarId}"]`)
   const existingTopic = sidebar.locator(`[data-sidebar-id="${existingTopicSidebarId}"]`)
   await roomItem.click()
   await roomItem.click()
+  const backToRooms = sidebar.locator('[data-sidebar-id="group-list"]')
+  await expect(backToRooms).toContainText('返回房间列表')
+  await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeHidden()
+  await expect(sidebar.getByRole('button', { name: '新建群聊' })).toBeHidden()
+  await expect(sidebar.getByRole('button', { name: '已归档' })).toBeHidden()
+  await expect(sidebar.getByRole('button', { name: '对话', exact: true })).toBeHidden()
+  await expect(sidebar.locator('.sidebar-footer')).toBeHidden()
   await expect(existingTopic).toContainText('发布检查')
   await expect(existingTopic).toContainText('发布检查')
   await expect(existingTopic).toHaveClass(/sidebar-item--topic/)
   await expect(existingTopic).toHaveClass(/sidebar-item--topic/)
 
 
@@ -567,7 +574,9 @@ test('selects existing group topics and creates a new protocol v4 topic on first
   await expect(timeline.getByText('请核对发布话题的独立历史。', { exact: true })).toBeVisible()
   await expect(timeline.getByText('请核对发布话题的独立历史。', { exact: true })).toBeVisible()
   await expect(timeline.getByText('大家好,检查一下群聊输入框。', { exact: true })).toHaveCount(0)
   await expect(timeline.getByText('大家好,检查一下群聊输入框。', { exact: true })).toHaveCount(0)
 
 
-  await sidebar.locator(`[data-sidebar-id="group-list"]`).click()
+  await backToRooms.click()
+  await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeVisible()
+  await expect(sidebar.getByRole('button', { name: '新建群聊' })).toBeVisible()
   await roomItem.click({ button: 'right' })
   await roomItem.click({ button: 'right' })
   const roomActions = page.getByRole('menu', { name: '群聊房间操作' })
   const roomActions = page.getByRole('menu', { name: '群聊房间操作' })
   await roomActions.getByRole('menuitem', { name: '新建话题' }).click()
   await roomActions.getByRole('menuitem', { name: '新建话题' }).click()

+ 1 - 0
tests/fixtures/fake-hermes.mjs

@@ -143,6 +143,7 @@ const server = createServer(async (request, response) => {
   }
   }
   if (url.pathname === '/api/session-unread') return json(response, 200, { items: { 'session-demo': 0 } })
   if (url.pathname === '/api/session-unread') return json(response, 200, { items: { 'session-demo': 0 } })
   if (url.pathname === '/api/plugins/yaoyao/v1/capabilities') return json(response, 200, { protocolVersion: 8, journalEpoch: epoch, latestCursor: groupCursor, limits: { maxAgentsPerRoom: 8, maxMessageBytes: 65536, maxMessagePageSize: 100, defaultMaxReplyRounds: 3, unlimitedReplyRoundsValue: -1, maxAgentDisplayNameLength: 100 }, eventTypes: ['message.upsert', 'topic.updated', 'room.activity', 'run.updated', 'agent.status', 'agent.updated'] })
   if (url.pathname === '/api/plugins/yaoyao/v1/capabilities') return json(response, 200, { protocolVersion: 8, journalEpoch: epoch, latestCursor: groupCursor, limits: { maxAgentsPerRoom: 8, maxMessageBytes: 65536, maxMessagePageSize: 100, defaultMaxReplyRounds: 3, unlimitedReplyRoundsValue: -1, maxAgentDisplayNameLength: 100 }, eventTypes: ['message.upsert', 'topic.updated', 'room.activity', 'run.updated', 'agent.status', 'agent.updated'] })
+  if (url.pathname === '/api/plugins/yaoyao/v1/topics/pinned') return json(response, 200, { items: groupTopics.filter(topic => topic.pinned), nextCursor: null })
   if (url.pathname === '/api/plugins/yaoyao/v1/rooms') return json(response, 200, { items: [room], nextCursor: null })
   if (url.pathname === '/api/plugins/yaoyao/v1/rooms') return json(response, 200, { items: [room], nextCursor: null })
   if (url.pathname === `/api/plugins/yaoyao/v1/rooms/${roomId}`) return json(response, 200, { ...room, agents: groupAgents, runs: [], pendingInteractions: [], latestCursor: groupCursor })
   if (url.pathname === `/api/plugins/yaoyao/v1/rooms/${roomId}`) return json(response, 200, { ...room, agents: groupAgents, runs: [], pendingInteractions: [], latestCursor: groupCursor })
   const agentPatchMatch = new RegExp(`^/api/plugins/yaoyao/v1/rooms/${roomId}/agents/([^/]+)$`).exec(url.pathname)
   const agentPatchMatch = new RegExp(`^/api/plugins/yaoyao/v1/rooms/${roomId}/agents/([^/]+)$`).exec(url.pathname)