From 288f36ab2b67c08b215bb4237fc6cb660461350e Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Thu, 3 Sep 2026 15:26:47 +0800 Subject: [PATCH 1/2] feat: split project and artifacts drawers Co-authored-by: CommandCodeBot --- app/thread-chat/branching/branchable-chat.tsx | 3 +- .../gate-3-harness/normalized-harness.tsx | 127 +++- .../net/persistence/workspace-state.ts | 5 +- .../artifacts/artifact-drawer.tsx | 104 --- .../orchestration/artifacts/artifact-open.ts | 44 ++ .../artifacts/artifacts-drawer.tsx | 333 ++++++++++ .../artifacts/layered-drawer.tsx | 289 +++++++++ .../artifacts/markdown-artifact-card.tsx | 19 +- .../artifacts/message-artifacts.tsx | 3 +- .../artifacts/project-drawer.tsx | 514 +++++++++++++++ .../orchestration/artifacts/project-panel.tsx | 613 ------------------ .../store-bound-artifacts-drawer.tsx | 106 +++ .../artifacts/store-bound-project-drawer.tsx | 92 +++ .../artifacts/store-bound-project-panel.tsx | 123 ---- .../orchestration/canvas/canvas-actions.ts | 3 +- .../orchestration/canvas/thread-canvas.tsx | 3 +- .../navigation/thread-chat-topbar.tsx | 66 +- .../orchestration/overlays/help-panel.tsx | 2 +- .../overlays/use-workspace-overlays.ts | 263 ++++++-- .../overlays/workspace-overlay-logic.ts | 90 ++- app/thread-chat/styles/drawer.css | 153 ++--- app/thread-chat/styles/selection.css | 2 +- app/thread-chat/styles/topbar.css | 13 +- app/thread-chat/thread-chat-demo.tsx | 84 ++- constants/project-workspace.ts | 3 + constants/workspace-drawers.ts | 20 + .../artifact-drawer-accessibility.test.mjs | 60 +- e2e/thread-chat/artifact-drawer-copy.test.mjs | 6 +- e2e/thread-chat/artifact-drawer-css.test.mjs | 51 +- e2e/thread-chat/message-artifacts.test.mjs | 32 + .../normalized-client-store.test.mjs | 44 +- .../project-panel-ui-contract.test.mjs | 141 ++-- .../project-panel-workspace-state.test.mjs | 2 +- e2e/thread-chat/thread-chat-topbar.test.mjs | 35 +- e2e/thread-chat/workspace-overlay.test.mjs | 119 +++- .../.openspec.yaml | 2 + .../split-project-artifacts-drawers/design.md | 167 +++++ .../proposal.md | 84 +++ .../specs/project-workspace/spec.md | 96 +++ .../specs/workspace-drawers/spec.md | 126 ++++ .../split-project-artifacts-drawers/tasks.md | 69 ++ 41 files changed, 2928 insertions(+), 1183 deletions(-) delete mode 100644 app/thread-chat/orchestration/artifacts/artifact-drawer.tsx create mode 100644 app/thread-chat/orchestration/artifacts/artifact-open.ts create mode 100644 app/thread-chat/orchestration/artifacts/artifacts-drawer.tsx create mode 100644 app/thread-chat/orchestration/artifacts/layered-drawer.tsx create mode 100644 app/thread-chat/orchestration/artifacts/project-drawer.tsx delete mode 100644 app/thread-chat/orchestration/artifacts/project-panel.tsx create mode 100644 app/thread-chat/orchestration/artifacts/store-bound-artifacts-drawer.tsx create mode 100644 app/thread-chat/orchestration/artifacts/store-bound-project-drawer.tsx delete mode 100644 app/thread-chat/orchestration/artifacts/store-bound-project-panel.tsx create mode 100644 constants/workspace-drawers.ts create mode 100644 openspec/changes/split-project-artifacts-drawers/.openspec.yaml create mode 100644 openspec/changes/split-project-artifacts-drawers/design.md create mode 100644 openspec/changes/split-project-artifacts-drawers/proposal.md create mode 100644 openspec/changes/split-project-artifacts-drawers/specs/project-workspace/spec.md create mode 100644 openspec/changes/split-project-artifacts-drawers/specs/workspace-drawers/spec.md create mode 100644 openspec/changes/split-project-artifacts-drawers/tasks.md diff --git a/app/thread-chat/branching/branchable-chat.tsx b/app/thread-chat/branching/branchable-chat.tsx index 49d85baf..4bd8569d 100644 --- a/app/thread-chat/branching/branchable-chat.tsx +++ b/app/thread-chat/branching/branchable-chat.tsx @@ -25,6 +25,7 @@ import { MessageArtifacts } from "../orchestration/artifacts/message-artifacts" import { AnchoredAssistantBody } from "./assistant/anchored-assistant-body" import type { MessageActionViewState } from "../chat/actions/message-action-types" import type { ThreadMessageActionCommands } from "../chat/actions/message-action-commands" +import type { OpenArtifact } from "../orchestration/artifacts/artifact-open" export interface BranchableChatProps { state: ThreadTreeState @@ -36,7 +37,7 @@ export interface BranchableChatProps { /** 统一意图:打开某会话(本列作为「来源列」参与放置策略)。 opts.keepSource:⌘/Ctrl 点击 = 保留本列,把目标开在紧邻右侧 */ onOpenThread: (targetId: string, opts?: { keepSource?: boolean }) => void - onOpenArtifact: (artifactId: string) => void + onOpenArtifact: OpenArtifact /** 面包屑就地回退(collapse 语义由 orchestration 实现) */ onCrumbNav: (targetId: string) => void /** ⇄ 把本列切换为任意会话(弹出 local 切换器,锚定在按钮上) */ diff --git a/app/thread-chat/gate-3-harness/normalized-harness.tsx b/app/thread-chat/gate-3-harness/normalized-harness.tsx index 31429b2e..b595ffba 100644 --- a/app/thread-chat/gate-3-harness/normalized-harness.tsx +++ b/app/thread-chat/gate-3-harness/normalized-harness.tsx @@ -1,7 +1,7 @@ "use client" import dynamic from "next/dynamic" -import React, { useEffect, useMemo, useRef, useState } from "react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { createConversationStore, type ConversationStore } from "../core/store" import { useConversationStore } from "../core/use-thread-store" import { @@ -34,7 +34,17 @@ import { kickoffQuestion } from "../net/prompt/prompt-pure" import { ThreadColumns } from "../orchestration/columns/thread-columns" import type { Slot } from "../orchestration/columns/placement" import { ThreadChatTopbar } from "../orchestration/navigation/thread-chat-topbar" -import { ArtifactDrawer } from "../orchestration/artifacts/artifact-drawer" +import { StoreBoundArtifactsDrawer } from "../orchestration/artifacts/store-bound-artifacts-drawer" +import { StoreBoundProjectDrawer } from "../orchestration/artifacts/store-bound-project-drawer" +import { + drawerSideForAnchor, + isNarrowDrawerViewport, + layerZIndex, + openLayer, + type DrawerId, + type DrawerSide, +} from "../orchestration/overlays/workspace-overlay-logic" +import type { OpenArtifact } from "../orchestration/artifacts/artifact-open" import type { CanvasChatActions } from "../orchestration/canvas/canvas-actions" import type { CanvasViewState } from "../orchestration/canvas/use-canvas-layout" import { @@ -139,13 +149,15 @@ export function NormalizedGate3Harness({ const [scenario, setScenario] = useState("normal") const [status, setStatus] = useState("开发 harness 已就绪") const [selection, setSelection] = useState(null) - const [drawerOpen, setDrawerOpen] = useState(false) + const [drawerStack, setDrawerStack] = useState([]) + const [artifactSide, setArtifactSide] = useState("right") const [activeArtifactId, setActiveArtifactId] = useState(null) const [forceCols, setForceCols] = useState(3) const [placementMode, setPlacementMode] = useState<"replace" | "fold">( "replace" ) const [titleDraft, setTitleDraft] = useState("规范化会话验收") + const rootRef = useRef(null) const columnsRef = useRef(null) const [columnWidths, setColumnWidths] = useState>({}) const [canvasViewState] = useState(() => ({ @@ -395,14 +407,69 @@ export function NormalizedGate3Harness({ const rootHasMessages = (tree.threads.main?.messages.length ?? 0) > 0 const branchCount = Math.max(0, Object.keys(tree.threads).length - 1) - const markdownCount = Object.values(tree.artifacts).filter( - (artifact) => artifact.kind === "markdown" - ).length + const artifactCount = state.artifactOrder.length const selectedScenarioLabel = SCENARIOS.find((entry) => entry.id === scenario)?.label ?? scenario + const narrowDrawers = isNarrowDrawerViewport(windowWidth ?? 1600) + const topDrawer = drawerStack.at(-1) ?? null + const projectDrawerIndex = drawerStack.indexOf("project") + const artifactsDrawerIndex = drawerStack.indexOf("artifacts") + const openDrawer = (id: DrawerId) => { + setDrawerStack((current) => + narrowDrawers ? [id] : openLayer(current, id) + ) + } + const toggleDrawer = (id: DrawerId) => { + setDrawerStack((current) => { + if (current.at(-1) === id) return current.filter((item) => item !== id) + return narrowDrawers ? [id] : openLayer(current, id) + }) + } + const closeDrawer = useCallback((id: DrawerId) => { + setDrawerStack((current) => current.filter((item) => item !== id)) + }, []) + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if ( + event.key !== "Escape" || + event.defaultPrevented || + event.isComposing || + event.repeat + ) + return + const top = drawerStack.at(-1) + if (top) closeDrawer(top) + } + document.addEventListener("keydown", onKeyDown) + return () => document.removeEventListener("keydown", onKeyDown) + }, [closeDrawer, drawerStack]) + const openArtifact = useCallback( + (artifactId, options) => { + setActiveArtifactId(artifactId) + setDrawerStack((current) => { + if (!current.includes("artifacts")) { + const width = + state.workspace.panelSizes.artifactDrawer ?? + Math.round(window.innerWidth / 3) + setArtifactSide( + options.source === "pointer" && options.anchorRect + ? drawerSideForAnchor({ + anchorLeft: options.anchorRect.left, + anchorRight: options.anchorRect.right, + viewportWidth: window.innerWidth, + drawerWidth: width, + }) + : "right" + ) + } + return narrowDrawers ? ["artifacts"] : openLayer(current, "artifacts") + }) + }, + [narrowDrawers, state.workspace.panelSizes.artifactDrawer] + ) return ( -
+
window.location.reload()} onToggleTreeList={() => setStatus("对话列表将在 Gate 4 接正式 Project list API") @@ -425,7 +492,8 @@ export function NormalizedGate3Harness({ onForceCols={setForceCols} onPlacementModeChange={setPlacementMode} onToggleThreadTree={() => openThread(GATE3_HARNESS_IDS.nestedThreadId)} - onToggleMarkdown={() => setDrawerOpen((open) => !open)} + onToggleProject={() => toggleDrawer("project")} + onToggleArtifacts={() => toggleDrawer("artifacts")} />