diff --git a/.commandcode/taste/taste.md b/.commandcode/taste/taste.md new file mode 100644 index 00000000..1e0e8821 --- /dev/null +++ b/.commandcode/taste/taste.md @@ -0,0 +1,2 @@ +# Taste +See [taste/taste.md](taste/taste.md) diff --git a/.commandcode/taste/taste/taste.md b/.commandcode/taste/taste/taste.md new file mode 100644 index 00000000..89bd6c3b --- /dev/null +++ b/.commandcode/taste/taste/taste.md @@ -0,0 +1,11 @@ +# Taste +- Communicates in Simplified Chinese with English technical terms mixed in; respond in Chinese. Confidence: 0.95 +- Answers numbered design questions point-by-point with terse, decision-oriented replies; comfortable being presented explicit options and trade-offs to pick from. Confidence: 0.7 +- Plan-gated: wants a written pre-proposal/design draft to review and approve before implementation begins ("你先把预方案给我看下"). Confidence: 0.85 +- Uses the codex CLI (@codex / `codex exec`) as an independent second reviewer for design documents before proceeding. Confidence: 0.8 +- Expects proposals grounded in the actual codebase — inspect how existing implementations handle a concern (e.g., Esc/focus in existing popups) before designing the new one. Confidence: 0.75 +- The thread-chatbot project captures design changes as OpenSpec change proposals (proposal + design + spec deltas via `openspec` CLI); UI changes that conflict with existing specs must rewrite those requirements as deltas. Confidence: 0.7 +- Non-intrusive UI is a core principle ("不干扰"): panels/drawers must avoid covering what the user is currently reading or just clicked — e.g., pick drawer side based on the click position. Confidence: 0.8 +- Prefers one adaptive mechanism that scales gracefully across the few-to-many spectrum (progressive disclosure) over separate fixed layouts for different scales. Confidence: 0.75 +- Prefers reusing existing patterns in the codebase and extracting shared components/abstractions ("或许可以把组件抽出来") over inventing parallel new ones. Confidence: 0.8 +- Cleans up legacy naming debt and dead code within the same refactor rather than deferring ("一起改掉"). Confidence: 0.75 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")} />