import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } from '@angular/core'; import { NavigationEnd, Router, RouterLink } from '@angular/router'; import { effect } from '@angular/core'; import { UiAvatar, UiButton, UiEmpty, UiInput, UiModal, UiSkeleton, UiSwitch, UiTooltip } from '../../../ui'; import { navItemActive } from '../../../desktop/shell/dk-top-nav/dk-top-nav'; import { ChatSession } from '../../../core/chat-session'; import { ChatSessionService } from '../../../core/chat-session.service'; import { Workspace, WorkspaceService } from '../../../core/workspace.service'; import { PromptModalService } from '../prompt-modal/prompt-modal.service'; import { NavMenuService } from '../../../core/nav-menu.service'; import { HoverScrollDirective } from './hover-scroll.directive'; import { ChatFontService } from '../../../core/chat-font.service'; import { NavCustomModalService } from '../nav-custom-modal/nav-custom-modal.service'; import { NewSessionModalService } from '../new-session-modal/new-session-modal.service'; import { activeSessionIdOf } from './active-session'; import { DeviceService } from '../../../core/device.service'; import { LiveService } from '../../../core/live.service'; import { PALETTES, PaletteService } from '../../../core/palette.service'; import { SessionService } from '../../../core/session.service'; import { ThemeService } from '../../../ui/theme'; import { UnreadService } from '../../../core/unread.service'; import { ChatService } from '../../../core/chat.service'; import { agentStatusTone } from '../../../core/api/models'; /** 工作区(用户手动新建的文件夹) */ /** 一次「选择容器并新建对话」的请求(由页面弹模态框处理) */ export interface NewSessionRequest { /** 目标工作区;`null` = 日常对话 */ workspaceId: string | null; } /** * 桌面·三段式会话树(工作区 / 日常对话 / 已归档)+ 底部用户卡 * * 依据 `docs/02-design/plans/2026-10-29-桌面布局改造-顶部导航与三段式会话树.md`: * · D2 工作区 = 用户手动新建的文件夹(自取名 / 重命名 / 删除) * · D4 日常对话 = 未归入工作区的会话,单一平铺 * · D5 各自归档:每个工作区内部、日常对话内部各有一份「已归档」 * · D6 用户卡在树底部,弹出:账号 / 主题 / 设置 * * ## 🔴 本轮(第 1 轮)范围:只做布局骨架 * * 工作区是**组件内内存态**(`signal`),刷新即丢失; * `ChatSession.workspaceId` 尚未落库,因此所有会话当前都归在「日常对话」下。 * 持久化与「会话→工作区」归属在第 3 轮接入 —— 接口(输入/输出)已按最终形态定义, * 第 3 轮只换数据源,模板不用改。 */ @Component({ selector: 'app-session-tree', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ NgTemplateOutlet, RouterLink, UiAvatar, UiButton, UiEmpty, UiInput, UiModal, UiSkeleton, UiSwitch, UiTooltip, HoverScrollDirective, ], template: `

将删除工作区「{{ removingWorkspace()?.name }}」。 @if (removingWorkspace(); as rw) { @if (sessionCountIn(rw.id) > 0) { 其中的 {{ sessionCountIn(rw.id) }} 条对话会被移回「日常对话」,不会被删除。 } @else { 这个工作区里没有对话。 } }

取消 删除工作区

「{{ archiving()?.title }}」会移到「已归档」里,随时可以取消归档恢复。

取消 归档

将从列表删除「{{ removing()?.title }}」,删除后不可恢复。

取消 删除
{{ s.title }} {{ s.kind === 'group' ? groupSubline(s) : agentNameOf(s) }} @if (workspaces().length > 0) { }
`, styles: [ ` :host { display: block; height: 100%; min-height: 0; overflow: hidden; } .tree { display: flex; flex-direction: column; height: 100%; min-height: 0; /* * 禁止选中/复制(2026-10-30 用户要求): * 左栏是导航区,误拖选会产生"文字被刷蓝"的噪音,看起来像界面出错。 * 输入框需要保留可选中,所以单独在 .inline-input / ui-input 上放行。 */ user-select: none; -webkit-user-select: none; } /* 输入类元素必须能选中文本,否则无法编辑/复制自己输入的内容 */ .tree input, .tree textarea { user-select: text; -webkit-user-select: text; /* * 底色用 canvas 与主区一致(2026-10-29 对齐 ChatGPT 侧栏观感): * 原来整块 surface 白底 + 右边框,像一张贴在左边的卡片; * 改成同底色、只留一道极淡的边,左栏与对话流更像同一块界面。 */ background: var(--canvas); border-right: 1px solid var(--edge); } /* * 搜索框(2026-10-29 对齐 ChatGPT 侧栏): * ui-input 自带明显边框,在侧栏里视觉重量过大 —— 用 :host 内的穿透无法做, * 改为外层容器收紧内边距,让输入框自己占满,减少两侧空白带来的"卡在中间"感。 */ /* ---- 品牌区 ---- */ /* * 品牌行(2026-10-30 按参考图调整): * · 去掉品牌图标,只留文字 * · 文字左缘与下方菜单**图标列**对齐(不是与图标左边距对齐) * · 行高 38 → 48,字号 16 → 20,整体更有"标题"分量 */ .brand-row { display: flex; align-items: center; gap: var(--sp-1); /* 左内边距与 .routes 一致,文字起点再对齐到图标列(见 .brand-word) */ padding: 0 var(--sp-2) 0 var(--sp-2); } .brand { display: flex; flex: 1; min-width: 0; align-items: center; min-height: 48px; /* 图标列宽 16 + 图标与文字间距 12 → 文字起点与菜单项文字列对齐 */ padding-left: var(--sp-3); flex-shrink: 0; text-decoration: none; color: var(--ink); } .brand-mark { display: inline-grid; place-items: center; /* 与折叠按钮同尺寸,两者在品牌行里水平对齐 */ width: 30px; height: 30px; border-radius: var(--r-sm); background: var(--nav-surface); color: var(--nav-brand); font-size: var(--fs-12); } .brand-word { font-size: var(--fs-20); font-weight: 700; letter-spacing: -0.5px; color: var(--ink); } /* * 折叠按钮(2026-10-30 按用户要求调整): * · **常驻显示**(原来悬停才出现,用户找不到入口) * · 尺寸放大到与品牌图标同级(30px),垂直居中对齐 */ .collapse-btn { display: grid; place-items: center; width: 30px; height: 30px; flex-shrink: 0; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--muted); font-size: var(--fs-14); cursor: pointer; transition: background-color var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard); } .collapse-btn:hover { background: var(--surface-hover); color: var(--ink); } /* ---- 一级路由 ---- */ .routes { display: grid; gap: 0; padding: 0 var(--sp-2) var(--sp-1); flex-shrink: 0; } .route { display: flex; align-items: center; gap: var(--sp-3); /* * 行高与字号(2026-10-30 按参考图放大):30→38、13→14。 * 参考图的导航行明显更松、字更实,30px 在高分屏上偏挤。 */ min-height: 38px; padding: 0 var(--sp-3); border-radius: var(--r-md); color: var(--ink-2); font-size: var(--fs-14); text-decoration: none; transition: background-color var(--motion-fast) var(--ease-standard); } .route:hover { background: var(--surface-hover); color: var(--ink); } .route.active { background: var(--surface-hover); color: var(--ink); font-weight: 600; } .route > i { /* 图标列固定 18px:品牌文字的左缘就是靠它算出来的对齐基准 */ width: 18px; flex-shrink: 0; text-align: center; color: var(--muted); font-size: var(--fs-14); } .route.active > i { color: var(--primary); } .route-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* * 「更多」按钮(2026-10-30 修悬停底色不满行): * 它是