Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
2ea2e53
docs(openspec): 收敛可删除 Quote 与缓存后续路线
hifizz Sep 2, 2026
8d38b4b
docs(project): add workspace design research
hifizz Aug 29, 2026
8cf8ee6
docs(project): 补充依赖支线成果交接场景
hifizz Aug 30, 2026
134bc12
docs(project): 收敛引用与 Outcome 初步方案
hifizz Aug 30, 2026
79e7404
docs(project): 冻结 Project MVP 范围与开发节奏
hifizz Aug 30, 2026
749048d
spec(project): 初始化 Project Workspace MVP OpenSpec
hifizz Aug 30, 2026
ab63e3f
spec(project): 添加 Project Workspace MVP proposal
hifizz Aug 30, 2026
4cfcd13
spec(project): 添加 Project Workspace MVP design
hifizz Aug 30, 2026
ea90c43
spec(project): 添加 Project Workspace MVP requirements
hifizz Aug 30, 2026
f83cbd8
spec(project): 添加 Project Workspace MVP tasks
hifizz Aug 30, 2026
9e5bc89
feat(project): add workspace data contracts
hifizz Aug 30, 2026
20b02a2
feat(project): add workspace server APIs
hifizz Aug 30, 2026
9e0dddf
feat(project): inject contract and project files
hifizz Aug 30, 2026
f05b2d6
ci(project): add temporary workspace validation
hifizz Aug 30, 2026
c2c2ae3
feat(project): add workspace client state
hifizz Aug 30, 2026
f4a67e4
ci(project): patch legacy workspace fixtures
hifizz Aug 30, 2026
527482c
test(project): update legacy workspace fixtures [skip ci]
github-actions[bot] Aug 30, 2026
00aed03
ci(project): rerun workspace validation
hifizz Aug 30, 2026
9b88366
feat(project): add project file upload client
hifizz Aug 31, 2026
e12530d
feat(project): unify project workspace panel
hifizz Aug 31, 2026
4dd7c56
style(project): add project panel workspace styles
hifizz Aug 31, 2026
9479b8d
feat(project): expose project workspace from topbar
hifizz Aug 31, 2026
11c157b
fix(project): keep project panel compatible with workspace shell
hifizz Aug 31, 2026
2f0ec0b
fix(project): preserve typed ui messages in attachment context
hifizz Aug 31, 2026
ba2698b
ci(project): keep legacy harness compatible with workspace client
hifizz Aug 31, 2026
cbf4a58
test(project): update legacy workspace fixtures [skip ci]
github-actions[bot] Aug 31, 2026
e8cae03
docs(openspec): record project workspace ui implementation
hifizz Aug 31, 2026
10d0f1b
test(project): complete legacy workspace client mock
hifizz Aug 31, 2026
e0936bd
test(project): complete workspace client fixture [skip ci]
github-actions[bot] Aug 31, 2026
6785fbf
ci(project): remove one-shot compatibility patch
hifizz Aug 31, 2026
69d8564
docs(openspec): mark completed project workspace tasks
hifizz Aug 31, 2026
835c535
test(project): cover contract generation boundaries
hifizz Aug 31, 2026
0828dc5
refactor(project): extract attachment content resolver
hifizz Aug 31, 2026
d7f4395
refactor(project): use shared attachment content resolver
hifizz Aug 31, 2026
437326e
docs(openspec): complete contract boundary and resolver tasks
hifizz Aug 31, 2026
5cbafd6
refactor(project): bind project panel to normalized store
hifizz Aug 31, 2026
c720c07
feat(project): bind project panel resources to conversation store
hifizz Aug 31, 2026
98a1141
ci(project): apply store-bound panel integration
hifizz Aug 31, 2026
d894953
ci(project): wire store-bound project panel
hifizz Aug 31, 2026
f449eee
ci(project): trigger store-bound panel patch
hifizz Aug 31, 2026
5083487
refactor(project): share project panel conversation store
hifizz Aug 31, 2026
d4b33d5
ci(project): restore read-only workspace validation
hifizz Aug 31, 2026
5545ae8
ci(project): remove one-shot panel patch workflow
hifizz Aug 31, 2026
d10fb37
feat(project): expose stable message anchors for provenance
hifizz Aug 31, 2026
b527379
feat(project): locate and highlight artifact source message
hifizz Aug 31, 2026
72523a2
test(project): preserve workspace state across panel refresh
hifizz Aug 31, 2026
16a4162
ci(project): verify project panel workspace isolation
hifizz Aug 31, 2026
ab33674
docs(openspec): complete project panel workspace tasks
hifizz Aug 31, 2026
224ee04
test(project): cover workspace schema commands and repositories
hifizz Aug 31, 2026
755421c
refactor(project): extract deterministic attachment context policy
hifizz Aug 31, 2026
3fe6a7a
refactor(project): apply pure attachment context policy
hifizz Aug 31, 2026
d1929b4
test(project): make pdf retrieval policy injectable
hifizz Aug 31, 2026
100fc35
test(project): cover deterministic project context policies
hifizz Aug 31, 2026
3b2ff22
ci(project): run project workspace database acceptance
hifizz Aug 31, 2026
6784f5b
test(project): cover workspace API isolation and bootstrap
hifizz Aug 31, 2026
16e9180
ci(project): run workspace API integration
hifizz Aug 31, 2026
52236b6
test(project): cover project panel UI contract
hifizz Aug 31, 2026
f7f321c
feat(evals): model project workspace context in agent cases
hifizz Aug 31, 2026
6482cab
feat(evals): seed project contract and project files
hifizz Aug 31, 2026
7b01649
test(evals): add project workspace coverage cases
hifizz Aug 31, 2026
b62b8cf
test(evals): verify project workspace production seed
hifizz Aug 31, 2026
46a256a
test(project): fix contract boundary generation setup
hifizz Aug 31, 2026
98e40fa
test(project): verify workspace history stability
hifizz Aug 31, 2026
6ccb5a6
test(project): prevent implicit artifact cross-thread context
hifizz Aug 31, 2026
baa07ad
ci(project): run complete project workspace acceptance suite
hifizz Aug 31, 2026
d785125
spec(project): clarify bootstrap isolation semantics
hifizz Aug 31, 2026
3de867a
spec(project): preserve empty bootstrap shell semantics
hifizz Aug 31, 2026
4af898c
test(project): align bootstrap shell isolation semantics
hifizz Aug 31, 2026
697c254
ci(project): run correct agent eval command
hifizz Aug 31, 2026
6bc8f80
eval(project): register workspace cases in manifests
hifizz Aug 31, 2026
921606f
spec(project): mark project workspace test suite complete
hifizz Aug 31, 2026
4e8fb6e
test(project): verify workspace migration on legacy data
hifizz Aug 31, 2026
6b4be38
ci(project): verify migration compatibility and schema sync
hifizz Aug 31, 2026
6445da0
ci(project): capture generated workspace migration snapshot
hifizz Aug 31, 2026
a2b23d0
ci(project): add one-time drizzle snapshot repair
hifizz Aug 31, 2026
ca6059f
fix(db): add project workspace drizzle snapshot
github-actions[bot] Aug 31, 2026
636b01b
ci(project): remove one-time snapshot repair
hifizz Aug 31, 2026
6b2853a
ci(project): restore migration sync validation
hifizz Aug 31, 2026
184bc64
spec(project): mark migration validation complete
hifizz Aug 31, 2026
b0540da
ci(project): add release validation gates
hifizz Aug 31, 2026
484d72e
refactor(project): remove obsolete duplicate artifact drawer
hifizz Aug 31, 2026
d5bb159
refactor(project): remove effect-driven panel state sync
hifizz Aug 31, 2026
b41befa
ci(project): lint active project panel implementation
hifizz Aug 31, 2026
368e34e
refactor(project): add thin legacy harness adapter
hifizz Aug 31, 2026
3c81c05
ci(project): lint legacy panel adapter too
hifizz Aug 31, 2026
3568780
feat(chat): group model selector by provider
hifizz Aug 30, 2026
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
135 changes: 135 additions & 0 deletions .github/workflows/project-workspace-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Project Workspace CI

on:
push:
branches:
- codex/research-project-workspace-design
workflow_dispatch:

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
DIRECT_URL: postgres://postgres:postgres@localhost:5432/postgres
BETTER_AUTH_SECRET: project-workspace-ci-secret-at-least-32-characters
MINIMAX_API_KEY: project-workspace-build-placeholder
MINIMAX_BASE_URL: https://example.invalid/v1
LLM_MODEL_ID: project-workspace-build-model
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm typecheck

- name: Targeted ESLint
run: >-
pnpm exec eslint
app/thread-chat/chat/chat-view.tsx
app/thread-chat/core/projections.ts
app/thread-chat/core/store.ts
app/thread-chat/core/types.ts
app/thread-chat/gate-3-harness/mock-v1-runtime.ts
app/thread-chat/net/client.ts
app/thread-chat/net/commands/conversation-commands.ts
app/thread-chat/net/project-file-upload.ts
app/thread-chat/orchestration/artifacts/artifact-drawer.tsx
app/thread-chat/orchestration/artifacts/project-panel.tsx
app/thread-chat/orchestration/artifacts/store-bound-project-panel.tsx
app/thread-chat/orchestration/navigation/thread-chat-topbar.tsx
app/thread-chat/thread-chat-demo.tsx
constants/project-workspace.ts
evals/agent/executors/production-harness.ts
evals/agent/schema.ts
lib/chat/attachment-content-resolver.ts
lib/chat/attachment-context-policy.ts
lib/chat/project-contract.ts
lib/chat/resolve-attachments.ts
lib/db/schema.ts
lib/thread-chat/application/compile-model-context.ts
lib/thread-chat/application/project-mutations.ts
lib/thread-chat/application/queries.ts
lib/thread-chat/contracts/commands.ts
lib/thread-chat/contracts/dto.ts
lib/thread-chat/persistence/artifact-repository.ts
lib/thread-chat/persistence/mappers.ts
lib/thread-chat/persistence/project-file-repository.ts
lib/thread-chat/server/handlers.ts
lib/thread-chat/streaming/finalize.ts
lib/thread-chat/streaming/generation-plan.ts
lib/thread-chat/streaming/run-generation.ts

- name: Verify generated migration is in sync
run: pnpm db:generate && git diff --exit-code -- drizzle

- name: Legacy data migration compatibility
run: node --import tsx e2e/thread-chat/project-workspace-migration-compatibility.test.mjs

- name: Pure project context policies
run: node --import tsx e2e/thread-chat/project-workspace-context.test.mjs

- name: Project panel UI contract
run: node e2e/thread-chat/project-panel-ui-contract.test.mjs

- name: Project panel workspace isolation
run: node --import tsx e2e/thread-chat/project-panel-workspace-state.test.mjs

- name: Project workspace eval harness
run: node --import tsx e2e/observability/project-workspace-eval-harness.test.mjs

- name: Prepare normalized test database
run: pnpm db:test:reset && pnpm db:test:migrate

- name: Project workspace schema and repository acceptance
run: node --import tsx e2e/thread-chat/project-workspace-db.test.mjs

- name: Project workspace API integration
run: node --import tsx e2e/thread-chat/project-workspace-api-db.test.mjs

- name: Project contract generation boundary
run: node --import tsx e2e/thread-chat/project-contract-generation-boundary.test.mjs

- name: Project workspace history stability
run: node --import tsx e2e/thread-chat/project-workspace-history-stability.test.mjs

- name: Project artifact context isolation
run: node --import tsx e2e/thread-chat/project-artifact-context-isolation.test.mjs

- name: Agent eval smoke
run: pnpm eval:agent

- name: Agent eval CI
run: pnpm eval:agent:ci

- name: Production build
run: pnpm build

- name: Validate OpenSpec
run: pnpm openspec:validate
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RouteContext } from "@/lib/thread-chat/server/route-utils"
import { handleRemoveProjectFile } from "@/lib/thread-chat/server/handlers"

export const dynamic = "force-dynamic"

type Context = RouteContext<{ projectId: string; attachmentId: string }>

export async function DELETE(request: Request, context: Context) {
const { projectId, attachmentId } = await context.params
return handleRemoveProjectFile(request, projectId, attachmentId)
}
11 changes: 11 additions & 0 deletions app/api/thread-chat/v1/projects/[projectId]/files/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RouteContext } from "@/lib/thread-chat/server/route-utils"
import { handleAddProjectFile } from "@/lib/thread-chat/server/handlers"

export const dynamic = "force-dynamic"

type Context = RouteContext<{ projectId: string }>

export async function POST(request: Request, context: Context) {
const { projectId } = await context.params
return handleAddProjectFile(request, projectId)
}
36 changes: 19 additions & 17 deletions app/thread-chat/chat/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,25 @@ export function ChatView({
<div className="lane">
{intro}
{messages.map((msg) => (
<MessageScroller.Item key={msg.id} messageId={msg.id}>
<ConversationMessage
threadId={threadId}
message={msg}
showRoleLabel
assistantBubbleClassName="bubble mt-3 mb-1"
renderAssistantBody={renderAssistantBody}
renderAfterMessage={renderAfterMessage}
onRetry={onRetry}
messageActionState={messageActionState}
messageCommands={messageCommands}
editableUserMessageId={editableUserMessageId}
regeneratableAssistantMessageId={
regeneratableAssistantMessageId
}
/>
</MessageScroller.Item>
<div key={msg.id} data-thread-chat-message-id={msg.id}>
<MessageScroller.Item messageId={msg.id}>
<ConversationMessage
threadId={threadId}
message={msg}
showRoleLabel
assistantBubbleClassName="bubble mt-3 mb-1"
renderAssistantBody={renderAssistantBody}
renderAfterMessage={renderAfterMessage}
onRetry={onRetry}
messageActionState={messageActionState}
messageCommands={messageCommands}
editableUserMessageId={editableUserMessageId}
regeneratableAssistantMessageId={
regeneratableAssistantMessageId
}
/>
</MessageScroller.Item>
</div>
))}
</div>
</MessageScroller.Content>
Expand Down
14 changes: 14 additions & 0 deletions app/thread-chat/chat/composer/thread-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
} from "@/components/ui/tooltip"
import {
CHAT_MODEL_PROVIDER_LABELS,
<<<<<<< HEAD
THREAD_CHAT_MODEL_GROUP_LABELS,
=======
>>>>>>> a30b2c9 (feat(chat): group model selector by provider)
THREAD_CHAT_MODELS,
} from "@/constants/model"
import { Bot } from "lucide-react"
Expand Down Expand Up @@ -64,12 +67,19 @@ const THREAD_CHAT_MODEL_OPTIONS: readonly ModelOption[] =
)
.map(({ model }) => ({
id: model.id,
<<<<<<< HEAD
name: model.name.replace(
`${CHAT_MODEL_PROVIDER_LABELS[model.provider]} · `,
""
),
providerId: model.provider,
providerName: THREAD_CHAT_MODEL_GROUP_LABELS[model.provider],
=======
name: model.name,
description: model.description,
providerId: model.provider,
providerName: CHAT_MODEL_PROVIDER_LABELS[model.provider],
>>>>>>> a30b2c9 (feat(chat): group model selector by provider)
}))

export interface ThreadModelSelectorProps {
Expand Down Expand Up @@ -140,7 +150,11 @@ export function ThreadModelSelector({
)}
<ModelSelector.Content
side="top"
<<<<<<< HEAD
className="thread-model-selector-content w-[28rem] max-w-[calc(100vw-1rem)]"
=======
className="thread-model-selector-content w-[34rem] max-w-[calc(100vw-2rem)]"
>>>>>>> a30b2c9 (feat(chat): group model selector by provider)
/>
</ModelSelector.Root>
)
Expand Down
10 changes: 1 addition & 9 deletions app/thread-chat/core/projections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ function projectMessageState(
}
}

/**
* 现有工作台组件以 `main` 作为根列的展示标识;规范化模型的根 Thread 则使用 UUID。
* 这个别名只存在于只读 UI facade,任何 v1 command/DTO 都继续使用真实 Thread ID。
*/
export function toConversationViewThreadId(
state: NormalizedThreadChatState,
threadId: string
Expand Down Expand Up @@ -184,15 +180,11 @@ export function projectArtifactDTO(
kind: artifact.kind,
...(artifact.language ? { lang: artifact.language } : {}),
content: artifact.content,
sourceThreadId: toConversationViewThreadId(
state,
state.messagesById[artifact.sourceMessageId]?.threadId ?? ""
),
sourceThreadId: toConversationViewThreadId(state, artifact.threadId),
sourceMessageId: artifact.sourceMessageId,
}
}

/** Gate 3 兼容 facade:既有组件不再读取整树持久化,只消费规范化 selector 投影。 */
export function projectConversationTree(
state: NormalizedThreadChatState
): ThreadTreeState {
Expand Down
47 changes: 46 additions & 1 deletion app/thread-chat/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
MessageDTO,
ProjectBootstrapDTO,
ProjectDTO,
ProjectFileDTO,
ThreadDTO,
} from "@/lib/thread-chat/contracts/dto"
import type {
Expand Down Expand Up @@ -57,6 +58,10 @@ function entitiesFromBootstrap(
const active = new Set(bootstrap.activeGenerationIds)
return {
project: bootstrap.project,
projectFilesById: Object.fromEntries(
bootstrap.files.map((file) => [file.attachmentId, file])
),
projectFileOrder: bootstrap.files.map((file) => file.attachmentId),
threadsById: Object.fromEntries(
bootstrap.threads.map((thread) => [thread.id, thread])
),
Expand All @@ -79,6 +84,7 @@ function entitiesFromBootstrap(
function emptyEntities(): ConversationEntitySnapshot {
return entitiesFromBootstrap({
project: null,
files: [],
threads: [],
messages: [],
artifacts: [],
Expand All @@ -91,6 +97,8 @@ function entitySnapshot(
): ConversationEntitySnapshot {
return structuredClone({
project: state.project,
projectFilesById: state.projectFilesById,
projectFileOrder: state.projectFileOrder,
threadsById: state.threadsById,
messagesById: state.messagesById,
messageIdsByThread: state.messageIdsByThread,
Expand Down Expand Up @@ -157,6 +165,30 @@ export function createConversationStore(input?: {
upsertProject(project: ProjectDTO) {
set({ project })
},
upsertProjectFile(file: ProjectFileDTO) {
set((state) => ({
projectFilesById: {
...state.projectFilesById,
[file.attachmentId]: file,
},
projectFileOrder: state.projectFileOrder.includes(file.attachmentId)
? state.projectFileOrder
: [file.attachmentId, ...state.projectFileOrder],
}))
},
removeProjectFile(attachmentId: string) {
set((state) => {
if (!state.projectFilesById[attachmentId]) return state
const projectFilesById = { ...state.projectFilesById }
delete projectFilesById[attachmentId]
return {
projectFilesById,
projectFileOrder: state.projectFileOrder.filter(
(id) => id !== attachmentId
),
}
})
},
upsertThread(thread: ThreadDTO) {
set((state) => ({
threadsById: { ...state.threadsById, [thread.id]: thread },
Expand All @@ -183,7 +215,7 @@ export function createConversationStore(input?: {
artifactsById: { ...state.artifactsById, [artifact.id]: artifact },
artifactOrder: state.artifactOrder.includes(artifact.id)
? state.artifactOrder
: [...state.artifactOrder, artifact.id],
: [artifact.id, ...state.artifactOrder],
}))
},
applyStreamSnapshot(messageId, message, throughSeq) {
Expand Down Expand Up @@ -340,6 +372,19 @@ export function createConversationStore(input?: {
sameValue(current.project, patch.after.project)
? structuredClone(patch.before.project)
: current.project,
projectFilesById: rollbackRecord(
current.projectFilesById,
patch.before.projectFilesById,
patch.after.projectFilesById
),
projectFileOrder:
!sameValue(
patch.before.projectFileOrder,
patch.after.projectFileOrder
) &&
sameValue(current.projectFileOrder, patch.after.projectFileOrder)
? structuredClone(patch.before.projectFileOrder)
: current.projectFileOrder,
threadsById: rollbackRecord(
current.threadsById,
patch.before.threadsById,
Expand Down
Loading