Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .commandcode/taste/taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Taste
See [taste/taste.md](taste/taste.md)
11 changes: 11 additions & 0 deletions .commandcode/taste/taste/taste.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion app/thread-chat/branching/branchable-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 切换器,锚定在按钮上) */
Expand Down
127 changes: 105 additions & 22 deletions app/thread-chat/gate-3-harness/normalized-harness.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -139,13 +149,15 @@ export function NormalizedGate3Harness({
const [scenario, setScenario] = useState<Gate3HarnessScenario>("normal")
const [status, setStatus] = useState("开发 harness 已就绪")
const [selection, setSelection] = useState<SelectionInfo | null>(null)
const [drawerOpen, setDrawerOpen] = useState(false)
const [drawerStack, setDrawerStack] = useState<DrawerId[]>([])
const [artifactSide, setArtifactSide] = useState<DrawerSide>("right")
const [activeArtifactId, setActiveArtifactId] = useState<string | null>(null)
const [forceCols, setForceCols] = useState<number | null>(3)
const [placementMode, setPlacementMode] = useState<"replace" | "fold">(
"replace"
)
const [titleDraft, setTitleDraft] = useState("规范化会话验收")
const rootRef = useRef<HTMLDivElement | null>(null)
const columnsRef = useRef<HTMLDivElement | null>(null)
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
const [canvasViewState] = useState<CanvasViewState>(() => ({
Expand Down Expand Up @@ -395,22 +407,77 @@ 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<OpenArtifact>(
(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 (
<div className="tc" data-gate3-normalized-harness="true">
<div ref={rootRef} className="tc" data-gate3-normalized-harness="true">
<ThreadChatTopbar
viewMode={viewMode}
showHelp
windowWidth={windowWidth}
forceCols={forceCols}
placementMode={placementMode}
branchCount={branchCount}
markdownCount={markdownCount}
artifactCount={artifactCount}
onNewConversation={() => window.location.reload()}
onToggleTreeList={() =>
setStatus("对话列表将在 Gate 4 接正式 Project list API")
Expand All @@ -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")}
/>

<aside
Expand All @@ -434,7 +502,7 @@ export function NormalizedGate3Harness({
position: "fixed",
zIndex: 80,
right: 12,
top: 52,
top: narrowDrawers ? 148 : 52,
display: "grid",
gap: 6,
width: 230,
Expand Down Expand Up @@ -539,10 +607,7 @@ export function NormalizedGate3Harness({
: undefined
}
onOpenThread={(target) => openThread(target)}
onOpenArtifact={(artifactId) => {
setActiveArtifactId(artifactId)
setDrawerOpen(true)
}}
onOpenArtifact={openArtifact}
onCrumbNav={openThread}
onOpenSwitcher={() =>
setStatus("Switcher 数据已由 normalized tree selector 提供")
Expand Down Expand Up @@ -587,10 +652,7 @@ export function NormalizedGate3Harness({
runtime.store.getState().setWorkspace({ view: "columns" })
openThread(threadId)
}}
onOpenArtifact={(artifactId) => {
setActiveArtifactId(artifactId)
setDrawerOpen(true)
}}
onOpenArtifact={openArtifact}
/>
)}

Expand All @@ -604,11 +666,32 @@ export function NormalizedGate3Harness({
maxExpanded={Math.max(1, (forceCols ?? 3) - 1)}
lastActiveOf={(threadId) => tree.threads[threadId]?.lastActive ?? 0}
/>
<ArtifactDrawer
state={tree}
open={drawerOpen}
<StoreBoundProjectDrawer
projectId={projectId}
store={runtime.store}
client={runtime.mock.client}
commands={runtime.commands}
open={projectDrawerIndex >= 0}
zIndex={layerZIndex(Math.max(0, projectDrawerIndex))}
topLayer={topDrawer === "project"}
narrow={narrowDrawers}
container={rootRef}
onActivate={() => openDrawer("project")}
onClose={() => closeDrawer("project")}
/>
<StoreBoundArtifactsDrawer
store={runtime.store}
open={artifactsDrawerIndex >= 0}
activeId={activeArtifactId}
onClose={() => setDrawerOpen(false)}
zIndex={layerZIndex(Math.max(0, artifactsDrawerIndex))}
side={artifactSide}
topLayer={topDrawer === "artifacts"}
narrow={narrowDrawers}
container={rootRef}
panelSizes={state.workspace.panelSizes}
setWorkspace={(next) => runtime.store.getState().setWorkspace(next)}
onActivate={() => openDrawer("artifacts")}
onClose={() => closeDrawer("artifacts")}
onSelect={setActiveArtifactId}
onLocate={(threadId) => {
runtime.store.getState().setWorkspace({ view: "columns" })
Expand Down
5 changes: 4 additions & 1 deletion app/thread-chat/net/persistence/workspace-state.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { WORKSPACE_DRAWER } from "@/constants/workspace-drawers"
import type { WorkspaceUiState } from "../../core/types"

const WORKSPACE_VERSION = 1
Expand Down Expand Up @@ -112,7 +113,9 @@ export function sanitizeWorkspaceState(
}
: {}),
...(typeof panelRaw.artifactDrawer === "number" &&
Number.isFinite(panelRaw.artifactDrawer)
Number.isFinite(panelRaw.artifactDrawer) &&
panelRaw.artifactDrawer >= WORKSPACE_DRAWER.artifactMinWidth &&
panelRaw.artifactDrawer <= WORKSPACE_DRAWER.artifactMaxPersistedWidth
? { artifactDrawer: panelRaw.artifactDrawer }
: {}),
},
Expand Down
104 changes: 0 additions & 104 deletions app/thread-chat/orchestration/artifacts/artifact-drawer.tsx

This file was deleted.

Loading
Loading