主题
工作区与聊天界面
本章目标:
- 理解
/workspace路由的页面树:认证守卫、侧边栏壳、聊天页(普通/自定义 agent)、agents 画廊与创建向导,以及它们如何围绕 thread + artifacts + todos 三个核心对象组织 UI。- 掌握聊天页的关键状态机:
isNewThread与isWelcomeMode的分工、useThreadStream的流式回填、optimistic 消息、artifacts/todos 面板如何由 thread state 驱动。- 看懂计划模式开关、模型/推理强度选择器、文件上传在
InputBox中如何映射到 LangGraph 的context,以及自定义 agent 的 setup/bootstrap 向导流程。
TL;DR
工作区是一个有状态的聊天应用:WorkspaceLayout 做服务端认证守卫,WorkspaceContent 提供侧边栏 + 命令面板的整体外壳。每个聊天页通过 useThreadChat 解析 thread id、useThreadStream 接管 LangGraph 流式会话,thread state 中的 messages/artifacts/todos 分别驱动消息列表、右侧可拖拽的 artifacts 面板和输入框上方的 To-do 卡片。InputBox 把 flash/thinking/pro/ultra 四种模式翻译成 thinking_enabled/is_plan_mode/subagent_enabled/reasoning_effort 等 context 字段。自定义 agent 走两步向导:先校验名字,再用 is_bootstrap 上下文与 LeadAgent 对话,最后通过 setup_agent 工具落盘。
Overview
工作区为什么围绕 thread + artifacts + todos 三个对象组织,而不是简单的"输入框 + 消息列表"?根因在于后端是一个 LangGraph super agent:一次对话不只产出文本,还会写出文件(artifacts)、维护一个待办计划(todos)、并在 ultra 模式下委派子代理。前端必须把这三类长生命周期产物都做成可观察、可导航的 UI 一等公民。
AgentThreadState 这个类型把这种组织方式固化下来——一个 thread 的状态就是 { title, messages, artifacts, todos } frontend/src/core/threads/types.ts:5-10。聊天页拿到的不是裸消息流,而是订阅整个 thread state:消息变了刷消息列表,thread.values.artifacts 变了刷右侧面板,thread.values.todos 变了刷 To-do 卡片。这就是为什么 ChatBox 用一个可拖拽的 ResizablePanelGroup 把聊天区和 artifacts 区做成左右双栏,而 ChatPage 把 TodoList 钉在 InputBox 上方——它们都是 thread state 的不同视图 frontend/src/components/workspace/chats/chat-box.tsx:104-176。
第二个设计动机是"新会话还没在后端存在"这一现实约束。LangGraph SDK 一旦拿到 thread id 就会立刻拉 /history,而新会话此时后端根本没有该 thread(issue #2746)。所以前端拆出两个状态:isNewThread(后端是否已创建,门控历史拉取)和 isWelcomeMode(纯视觉:居中输入 + hero),提交瞬间翻 isWelcomeMode 让 UI 立即动画,但 isNewThread 要等流真正 onStart 才翻 frontend/src/app/workspace/chats/[thread_id]/page.tsx:38-91。
Architecture
/workspace 是一个三层嵌套布局:外层服务端认证守卫 → 中层侧边栏外壳 → 内层各功能页;聊天页自身再叠一层 Provider 栈(Subtasks / Artifacts / PromptInput)。
Source:
- 认证守卫:frontend/src/app/workspace/layout.tsx:12-60
- 侧边栏外壳:frontend/src/app/workspace/workspace-content.tsx:17-35
- 默认重定向:frontend/src/app/workspace/page.tsx:8-20
- 聊天页 Provider 栈:frontend/src/app/workspace/chats/[thread_id]/layout.tsx:7-19
- agent 聊天页 Provider 栈:frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/layout.tsx:7-19
- 侧边栏组件:frontend/src/components/workspace/workspace-sidebar.tsx:17-38
WorkspaceLayout 是 server component,getServerSideUser() 返回的 tag 决定走向:authenticated 才渲染 AuthProvider + WorkspaceContent,needs_setup/system_setup_required 重定向到 /setup,unauthenticated 去 /login,gateway_unavailable 渲染降级页 frontend/src/app/workspace/layout.tsx:15-58。WorkspaceContent 从 cookie 读 sidebar_state 恢复侧边栏开合,再用 QueryClientProvider + SidebarProvider 包裹 WorkspaceSidebar 与 SidebarInset,并挂载全局 CommandPalette 与 Toaster frontend/src/app/workspace/workspace-content.tsx:20-34。
Components / Subsystems
聊天页(chats/[thread_id])
职责:解析 thread id、接管流式会话、把 thread state 渲染成消息流 + artifacts + todos + 输入框。
- 路由跳板:
/workspace直接redirect("/workspace/chats/new")(静态站点模式则跳第一个 demo thread)frontend/src/app/workspace/page.tsx:8-20。 - thread id 解析:
useThreadChat把路径段new映射为新生成的uuid(),并维护isNewThread;它特意防御history.replaceState后useParams仍返回"new"的竞态,避免把非法 id 传给useStream触发 422 frontend/src/components/workspace/chats/use-thread-chat.ts:8-40。 - 页面骨架:
ChatBox双栏 + header(ThreadTitle/TokenUsageIndicator/ExportTrigger/ArtifactTrigger)+MessageList+TodoList+InputBox,isWelcomeMode控制输入框居中还是贴底 frontend/src/app/workspace/chats/[thread_id]/page.tsx:129-256。 onStart回调拿到后端创建的 thread id 后,用原生history.replaceState改 URL 而非 Next.js router——后者会导致组件重挂载丢状态 frontend/src/app/workspace/chats/[thread_id]/page.tsx:86-91。- Provider 栈:聊天页
layout.tsx包裹SubtasksProvider(子代理实时消息)、ArtifactsProvider(选中/开合)、PromptInputProvider(输入框受控态)frontend/src/app/workspace/chats/[thread_id]/layout.tsx:12-18。
agents 画廊与自定义 agent 工作流(agents/)
职责:列出已保存 agent、引导用户用对话方式创建新 agent。
- 画廊:
AgentGallery用useAgents()(GET /api/agents)拉列表,空态引导新建,有数据则网格渲染AgentCardfrontend/src/components/workspace/agents/agent-gallery.tsx:12-69;hooks 全部基于 TanStack Query frontend/src/core/agents/hooks.ts:12-27。 - 创建向导(setup/bootstrap):
NewAgentPage分name→chat两步。第一步checkAgentName校验名字唯一性与 API 是否启用;第二步用useThreadStream开一个context.is_bootstrap = true、mode: "flash"的会话与 LeadAgent 对话 frontend/src/app/workspace/agents/new/page.tsx:73-177。 - 落盘:用户点"保存"会发一条隐藏消息(
hide_from_ui: true)请求 LeadAgent 调用setup_agent工具;onToolEnd监听name === "setup_agent",触发getAgentWithRetry(带重试退避)轮询新建结果 frontend/src/app/workspace/agents/new/page.tsx:100-111 frontend/src/app/workspace/agents/new/page.tsx:199-232。 - agent 聊天页:
agents/[agent_name]/chats/[thread_id]复用同一套ChatBox/InputBox,在 header 加 agent 徽标,并把agent_name注入context与sendMessagefrontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx:65-117。useThreadStream的onCreated还会把agent_name写进 thread metadata frontend/src/core/threads/hooks.ts:228-237。
threads 侧栏与会话管理 UI
职责:导航(Chats/Agents 入口)+ 最近会话列表 + 重命名/分享/导出/删除。
- 导航项:
WorkspaceNavChatList两个入口,按 pathname 判定 active frontend/src/components/workspace/workspace-nav-chat-list.tsx:15-43。 - 最近会话:
RecentChatList用useThreads()(分页搜索,默认按updated_at desc、limit:50)拉列表 frontend/src/core/threads/hooks.ts:787-852;每项的下拉菜单提供重命名(useRenameThread,乐观更新)、分享(复制链接,localhost 用 Vercel URL)、导出 markdown/json、删除(useDeleteThread,删除当前会话后跳到相邻会话)frontend/src/components/workspace/recent-chat-list.tsx:79-161。 - 路由工具:
pathOfThread根据 thread 的context.agent_name或metadata.agent_name决定生成/workspace/agents/<name>/chats/<id>还是/workspace/chats/<id>,这是 agent 会话与普通会话共用列表却能正确跳转的关键 frontend/src/core/threads/utils.ts:13-34。 - 独立搜索页:
chats/page.tsx提供一个大搜索框,按titleOfThread客户端过滤useThreads()结果 frontend/src/app/workspace/chats/page.tsx:18-71。
artifacts 面板
职责:把 thread 产出的文件做成可拖拽展开的右侧详情区。
- 状态由
ArtifactsProvider持有:artifacts列表、selectedArtifact、open、autoSelect/autoOpen;select()选中文件时会同时收起侧边栏给详情腾空间 frontend/src/components/workspace/artifacts/context.tsx:34-88。 ChatBox用react-resizable-panels实现 chat/artifacts 60:40 ↔ 100:0 的过渡;切换 thread 时deselect(),并把thread.values.artifacts同步进 context frontend/src/components/workspace/chats/chat-box.tsx:45-101。- header 的
ArtifactTrigger仅在有 artifacts 时显示,点击setArtifactsOpen(true)展开面板 frontend/src/components/workspace/artifacts/artifact-trigger.tsx:9-30。详情页ArtifactFileDetail按文件类型在代码编辑器 / Markdown 预览间切换,并对.skill提供安装入口 frontend/src/components/workspace/artifacts/artifact-file-detail.tsx:47-60。
todos / tasks UI
职责:把 LeadAgent 的计划与子代理实时进度可视化。
- To-do 卡片:
TodoList直接渲染thread.values.todos(Todo = { content?, status? }),status决定指示器与文字样式;它是一个可折叠的钉在输入框上方的卡片 frontend/src/components/workspace/todo-list.tsx:14-99 frontend/src/core/todos/types.ts:1-4。聊天页用hasTodos = (thread.values.todos?.length ?? 0) > 0决定是否挂载 frontend/src/app/workspace/chats/[thread_id]/page.tsx:127。 - 子任务进度:
SubtasksProvider用一个Record<string, Subtask>持有每个子代理的最新消息;useThreadStream的onCustomEvent收到type: "task_running"自定义事件时调用updateSubtask把子代理最新 AIMessage 写进去 frontend/src/core/tasks/context.tsx:17-53 frontend/src/core/threads/hooks.ts:303-317。
Data Flow
下图展示用户在聊天页发一条带附件的消息,到 UI 状态、core hook、流式回填、artifacts/todos 更新的完整链路。
关键细节:
- optimistic 消息:
sendMessage立即插入一条乐观 human 消息(以及附件上传中的占位 ai),mergeMessages把 history、流式thread.messages与 optimistic 三者去重合并;乐观消息要等服务端的 human 消息真正到达才清除,避免闪烁 frontend/src/core/threads/hooks.ts:411-426 frontend/src/core/threads/hooks.ts:95-126。 - 计划模式映射:
thread.submit的context由当前context.mode推导——thinking_enabled = mode !== "flash"、is_plan_mode = mode === "pro" || mode === "ultra"、subagent_enabled = mode === "ultra",reasoning_effort按 ultra/pro/thinking 取 high/medium/low frontend/src/core/threads/hooks.ts:593-609。 - 历史分页:
useThreadHistory按 run 逐个倒序拉/api/threads/<id>/runs/<run>/messages,过滤掉caller以middleware:开头的消息,hasMore控制"加载更多" frontend/src/core/threads/hooks.ts:660-785。 - 标题回填:
onUpdateEvent监到update.title时,直接 patch TanStack Query 缓存里对应 thread 的标题,侧栏立即更新 frontend/src/core/threads/hooks.ts:275-301;ThreadTitle同步设置document.titlefrontend/src/components/workspace/thread-title.tsx:20-49。
速查表
| 页面 / 组件 | 角色 | Source |
|---|---|---|
WorkspaceLayout | server 认证守卫,按 tag 重定向 | layout.tsx:12-60 |
WorkspaceContent | 侧边栏外壳 + 命令面板 + Toaster | workspace-content.tsx:17-35 |
chats/[thread_id]/page | 普通聊天页主壳 | page.tsx:33-260 |
agents/[agent_name]/chats/[thread_id]/page | 自定义 agent 聊天页 | page.tsx:34-271 |
agents/page / AgentGallery | agent 画廊列表 | agent-gallery.tsx:12-69 |
agents/new/page | 两步 setup/bootstrap 创建向导 | new/page.tsx:73-394 |
chats/page | 会话搜索/列表页 | chats/page.tsx:18-71 |
WorkspaceSidebar | Header + NavChatList + RecentChatList | workspace-sidebar.tsx:17-38 |
RecentChatList | 最近会话 + 重命名/分享/导出/删除 | recent-chat-list.tsx:61-301 |
ChatBox | 可拖拽 chat/artifacts 双栏 | chat-box.tsx:26-178 |
InputBox | 输入框 + 模式/模型/推理强度 + 上传 | input-box.tsx:102-892 |
TodoList | 计划卡片(钉在输入框上方) | todo-list.tsx:14-99 |
ArtifactsProvider / ArtifactTrigger | artifacts 选中/开合状态 | context.tsx:34-96 |
useThreadChat | thread id 解析 / new 防御 | use-thread-chat.ts:8-40 |
useSpecificChatMode | ?mode=skill 预填技能创建 prompt | use-chat-mode.ts:10-41 |
Common Pitfalls / Tips
- 不要用 Next.js router 改 thread URL:新会话创建后必须用原生
history.replaceState;用router.push会重挂载页面、丢掉流状态和 optimistic 消息 frontend/src/app/workspace/chats/[thread_id]/page.tsx:89-90。 isNewThread与isWelcomeMode别混用:前者门控历史/token 拉取(等后端真正建好 thread),后者只是视觉布局(提交瞬间翻)。提前翻isNewThread会让 SDK 立刻请求一个还不存在的 thread 历史(issue #2746)frontend/src/app/workspace/chats/[thread_id]/page.tsx:38-64。- agent 会话靠 metadata 区分跳转:
pathOfThread优先读context.agent_name再读metadata.agent_name;useThreadStream.onCreated会主动把agent_name写进 thread metadata,否则侧栏点击会跳错路由 frontend/src/core/threads/utils.ts:13-34 frontend/src/core/threads/hooks.ts:228-237。 - 保存 agent 用隐藏消息:创建向导的"保存"是发一条
additional_kwargs.hide_from_ui = true的消息触发setup_agent工具,sendMessage据此跳过 optimistic UI 渲染;落盘有写后读延迟,所以用getAgentWithRetry退避轮询 frontend/src/app/workspace/agents/new/page.tsx:199-232 frontend/src/core/threads/hooks.ts:460-485。 - flash 模式无法选推理强度:
InputBox只有supportReasoningEffort && context.mode !== "flash"才渲染推理强度菜单;切到不支持 thinking 的模型会被getResolvedMode强制回 flash frontend/src/components/workspace/input-box.tsx:89-100 frontend/src/components/workspace/input-box.tsx:694-695。 - 会话历史会过滤 middleware 消息:
useThreadHistory丢弃metadata.caller以middleware:开头的消息,所以历史里看不到 summarization 等中间件产物,这是有意的 frontend/src/core/threads/hooks.ts:721-723。
References
- frontend/src/app/workspace/layout.tsx — 服务端认证守卫
- frontend/src/app/workspace/chats/[thread_id]/page.tsx — 聊天页主壳与状态机
- frontend/src/core/threads/hooks.ts —
useThreadStream/useThreads/历史/重命名/删除 - frontend/src/components/workspace/input-box.tsx — 输入框、模式开关、模型选择、上传
- frontend/src/components/workspace/chats/chat-box.tsx — 可拖拽 chat/artifacts 双栏
- frontend/src/app/workspace/agents/new/page.tsx — 自定义 agent 两步创建向导
- frontend/src/components/workspace/recent-chat-list.tsx — 侧栏会话管理 UI
- frontend/src/core/tasks/context.tsx — 子任务实时进度 Provider
- frontend/src/components/workspace/todo-list.tsx — To-do 计划卡片
Related Pages
| 章节 | 关系 |
|---|---|
| 28-前端技术栈与应用结构 | 上游:本章页面所处的 Next.js App Router 结构、Provider 体系与目录约定 |
| 29-AI消息流与流式渲染 | 平行:MessageList 如何把本章 useThreadStream 回填的消息渲染成气泡/工具卡片 |
| 31-前端核心服务层 | 下游:本章用到的 getAPIClient、core/threads、core/agents、settings 等服务层细节 |
| 19-子代理委派系统 | 后端:本章 task_running 自定义事件与 ultra 模式 subagent_enabled 对应的子代理委派机制 |