From 1d7f2a3e1b2b77f4f2690b80fa8b7481c9d15f87 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 22 Oct 2026 03:18:08 +0000 Subject: [PATCH] Add the no-project conversation view backed by scratch sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Accounts without project.create need isolated general-agent chats. The sidebar groups every scratch session under one "无项目对话" entry instead of the project switcher, and the client no longer auto-creates a default project. Evidence: npx vitest run (64 files, 677 tests) and npx tsc -b --noEmit pass locally (SELF); CI on the PR head is authoritative. Impact: createTask uses POST /api/scratch-sessions in scratch mode or when no real project exists; scratch rows hide cross-read toggles and project settings; "新建项目…" stays gated by project.create. Refs: Agent-System-Dev docs/superpowers/specs/2026-10-22-scratch-session-general-agent-design.md; agent-exec receipt u3-run5 (cursor, success) Co-authored-by: Cursor --- src/api.ts | 1 + src/components/TaskSidebar.tsx | 43 +++++++---- src/contracts.ts | 2 + src/store.test.ts | 24 +++++- src/store.ts | 112 ++++++++++++++++++++++------ test/scratch-session.test.ts | 129 +++++++++++++++++++++++++++++++++ 6 files changed, 270 insertions(+), 41 deletions(-) create mode 100644 test/scratch-session.test.ts diff --git a/src/api.ts b/src/api.ts index eff28e4..4aea859 100644 --- a/src/api.ts +++ b/src/api.ts @@ -117,6 +117,7 @@ export const api = { explainSystemAction: (action: string) => auth.fetch(`${BASE}/api/authz/explain?action=${encodeURIComponent(action)}`).then(r => json<{ allowed: boolean; actionId: string; source?: string; reason?: string; authorityEpoch: number }>(r)), listProjects: (signal?: AbortSignal) => auth.fetch(`${BASE}/api/projects`, signal ? { signal } : undefined).then(r => json(r)), createProject: (name: string) => auth.fetch(`${BASE}/api/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }).then(r => json(r)), + createScratchSession: (title?: string) => auth.fetch(`${BASE}/api/scratch-sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(title ? { title } : {}) }).then(r => json<{ project: Project; conversation: Conversation }>(r)), renameProject: (id: string, name: string) => auth.fetch(`${BASE}/api/projects/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }).then(r => json(r)), deleteProject: (id: string) => auth.fetch(`${BASE}/api/projects/${id}`, { method: 'DELETE' }).then(checked), diff --git a/src/components/TaskSidebar.tsx b/src/components/TaskSidebar.tsx index b14f2a3..9421142 100644 --- a/src/components/TaskSidebar.tsx +++ b/src/components/TaskSidebar.tsx @@ -55,6 +55,7 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, const artifacts = useStore(s => s.artifacts) const currentConvId = useStore(s => s.currentConvId) const currentProjectId = useStore(s => s.currentProjectId) + const scratchMode = useStore(s => !!s.scratchMode) const showArchived = useStore(s => s.ui.showArchived) const serverWorkspaces = useStore(s => s.serverWorkspaces) /** 审计入口能力门控(增量 2):命令面探测通过才展示「追溯审计」。 */ @@ -74,13 +75,15 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, const createConversation = async (projectId?: string) => { try { + if (projectId && scratchMode) await store.openProject(projectId) await store.createTask(projectId) onConversationOpen?.() } catch { /* store owns the actionable failure toast */ } } const tools = buildWorkspaceEntries(serverWorkspaces, { rdmAudit: rdmAuditAvailable, rdmCockpit: rdmCockpitAvailable }) - const visible = filterTasks(tasks.filter(t => t.projectId === currentProjectId), q) + const scratchTasks = tasks.filter(t => t.scratch) + const visible = filterTasks(scratchMode ? scratchTasks : tasks.filter(t => t.projectId === currentProjectId && !t.scratch), q) const pinned = visible.filter(t => t.pinned) const rest = visible.filter(t => !t.pinned) const currentProject = projects.find(p => p.id === currentProjectId) @@ -99,10 +102,10 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, - + } ) @@ -121,7 +124,7 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, {rowActions(t)} @@ -136,9 +139,9 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen,
@@ -147,7 +150,8 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, {/* 主入口创建会话;项目创建保留在次级菜单。 */}
- @@ -156,12 +160,12 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen,
- setQ(e.target.value)} /> + setQ(e.target.value)} />
- 本项目会话 + {scratchMode ? '无项目对话' : '本项目会话'} @@ -195,7 +199,8 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, )} {rest.length === 0 && pinned.length === 0 && ( -
{noProjects ? emptyProjectsNotice(projectsLoaded, canCreate) +
{scratchMode ? '开始一个通用对话;无项目对话彼此独立,不会使用项目文档与知识。' + : noProjects ? emptyProjectsNotice(projectsLoaded, canCreate) : workError ?? '从「新建对话」开始,描述你想完成的工作。'}
)} {rest.map(t => taskRow(t))} @@ -215,7 +220,7 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen, {entry.shortcut} ))} - {currentProject && ( + {currentProject && !scratchMode && (