Compare commits

...
4 Commits
Author SHA1 Message Date
weichen ac5cf6b322 Merge pull request 'Deliver increment C Frontend work: formal RDM audit workspace' (#125) from feature/rdm-two-trees-followup-w10 into main
governance / governance (push) Successful in 17s
ci / test-and-build (push) Successful in 1m19s
2026-10-22 06:25:22 +00:00
weichen a82a1e18de Merge remote-tracking branch 'origin/main' into feature/rdm-two-trees-followup-w10
governance / governance (pull_request) Successful in 18s
ci / test-and-build (pull_request) Successful in 52s
2026-10-22 06:23:25 +00:00
weichen 279b2d02a2 Merge pull request 'Keep the scratch flag out of the Core-generated contracts' (#124) from fix/scratch-conversation-view-type into main
governance / governance (push) Successful in 27s
ci / test-and-build (push) Successful in 1m8s
2026-10-22 06:07:28 +00:00
weichenandCursor 6e611c5de0 Keep the scratch flag out of the Core-generated contracts
governance / governance (pull_request) Successful in 19s
ci / test-and-build (pull_request) Successful in 49s
Why: src/contracts.ts is regenerated from Agent-System-Core by the Dev
composite; the scratch field added there is dropped on regeneration and
tsc then fails in make build.

Evidence: contracts.ts equals the Core-generated output; npx tsc -b
--noEmit, npx vitest run (685 tests) and check-knip pass locally (SELF).

Impact: ConversationView (Conversation & { scratch?: boolean }) in
api.ts carries the server-local flag, mirroring the Backend
conversationView; no behavior change.

Refs: Agent-System-Frontend#123, Agent-System-Dev#691;
agent-exec receipt u3f-run (cursor, success)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 06:06:01 +00:00
6 changed files with 22 additions and 22 deletions
+6 -4
View File
@@ -186,6 +186,8 @@ async function json<T>(res: Response): Promise<T> {
return res.json() as Promise<T>
}
export type ConversationView = Conversation & { /** 无项目对话:服务端内部项目只承载这一条会话,不出现在项目列表里。 */ scratch?: boolean }
export const api = {
permissionCatalog: () => auth.fetch(`${BASE}/api/admin/permissions/catalog`).then(r => json<{ permissions: PermissionDescriptor[] }>(r)).then(d => d.permissions ?? []),
listRoles: (request: { query?: string; mode?: 'exact' | 'fuzzy'; status?: 'all' | 'enabled' | 'disabled'; action?: string; cursor?: string; limit?: number } = {}) => {
@@ -220,13 +222,13 @@ 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<Project[]>(r)),
createProject: (name: string) => auth.fetch(`${BASE}/api/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }).then(r => json<Project>(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)),
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: ConversationView }>(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<Project>(r)),
deleteProject: (id: string) => auth.fetch(`${BASE}/api/projects/${id}`, { method: 'DELETE' }).then(checked),
listConversations: (projectId: string) => auth.fetch(`${BASE}/api/projects/${projectId}/conversations`).then(r => json<Conversation[]>(r)),
listConversations: (projectId: string) => auth.fetch(`${BASE}/api/projects/${projectId}/conversations`).then(r => json<ConversationView[]>(r)),
// 跨项目任务列表(方案 A 侧栏):默认仅未归档;includeArchived 含归档。
listTasks: (includeArchived?: boolean) => auth.fetch(`${BASE}/api/conversations${includeArchived ? '?archived=1' : ''}`).then(r => json<Conversation[]>(r)),
listTasks: (includeArchived?: boolean) => auth.fetch(`${BASE}/api/conversations${includeArchived ? '?archived=1' : ''}`).then(r => json<ConversationView[]>(r)),
setConversationFlags: (id: string, flags: { pinned?: boolean; archived?: boolean; allowCrossRead?: boolean; crossPrivate?: boolean }) => auth.fetch(`${BASE}/api/conversations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(flags) }).then(r => { if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) }),
setPermissionMode: (id: string, permissionMode: string) => auth.fetch(`${BASE}/api/conversations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ permissionMode }) }).then(r => { if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) }),
checkpoints: (id: string, messageId: string) => auth.fetch(`${BASE}/api/conversations/${id}/checkpoints?messageId=${encodeURIComponent(messageId)}`).then(r => json<{ restored?: string[]; skipped?: string[] }>(r)),
@@ -292,7 +294,7 @@ export const api = {
setConversationProfile: (id: string, profileId: string) => auth.fetch(`${BASE}/api/conversations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profileId }) }).then(r => json<Conversation>(r)),
conversationDetail: (id: string) => auth.fetch(`${BASE}/api/conversations/${id}`).then(r =>
json<{ conversation: Conversation; messages: Message[]; runs: Run[]; activeHitl: HITLCard[]; lastSeq: number; resumeSeq?: number }>(r)),
json<{ conversation: ConversationView; messages: Message[]; runs: Run[]; activeHitl: HITLCard[]; lastSeq: number; resumeSeq?: number }>(r)),
runEventsPage: (convId: string, runId: string, after: number, signal?: AbortSignal) =>
auth.fetch(`${BASE}/api/conversations/${encodeURIComponent(convId)}/runs/${encodeURIComponent(runId)}/events?after=${after}&limit=500`, { signal })
+3 -4
View File
@@ -3,9 +3,8 @@
import { useEffect, useRef, useState } from 'react'
import { store, useStore } from '../store'
import { auth } from '../auth'
import { api } from '../api'
import { api, type ConversationView } from '../api'
import { Icon } from '@ui'
import type { Conversation } from '../contracts'
import { relativeTime, filterTasks } from '../tasklist'
import {
canCreateProject, emptyProjectsNotice, projectCapabilitySummary, projectStage, projectStateLabel, projectWorkError,
@@ -96,7 +95,7 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen,
const syncNotice = syncStatus === 'degraded' ? '项目列表同步异常,正在自动重试'
: syncStatus === 'offline' ? '网络已断开,恢复后自动同步项目列表' : null
const rowActions = (t: Conversation) => (
const rowActions = (t: ConversationView) => (
<span className="tsb-row-acts">
<button className="tsb-row-act" title="置顶 / 取消置顶" onClick={e => { e.stopPropagation(); store.pinTask(t.id, !t.pinned) }}><Icon name="pin" size={13} /></button>
<button className="tsb-row-act" title={t.archived ? '恢复' : '归档'} onClick={e => { e.stopPropagation(); store.archiveTask(t.id, !t.archived) }}>
@@ -118,7 +117,7 @@ export function TaskSidebar({ onCollapse = store.toggleLeft, onConversationOpen,
onConversationOpen?.()
}
const taskRow = (t: Conversation) => (
const taskRow = (t: ConversationView) => (
<div key={t.id} className={`tsb-task ${t.id === currentConvId ? 'active' : ''} ${t.archived ? 'archived' : ''}`}
onContextMenu={e => { e.preventDefault(); setMenu({ kind: `row:${t.id}`, anchor: e.currentTarget as HTMLElement }) }}>
<button className="tsb-task-open" onClick={() => { store.openTask(t.id); onConversationOpen?.() }}>
-2
View File
@@ -18,8 +18,6 @@ export interface Conversation {
allowCrossRead?: boolean; crossPrivate?: boolean;
permissionMode?: 'default' | 'plan' | 'auto_edit' | '';
sessionNote?: string;
/** 无项目对话:服务端内部项目只承载这一条会话,不出现在项目列表里。 */
scratch?: boolean;
createdAt: string; updatedAt: string;
}
+4 -3
View File
@@ -4,10 +4,11 @@ import {
PROJECT_SYNC_INTERVAL_MS, PROJECT_SYNC_RENDER_BUDGET_MS, PROJECT_SYNC_REQUEST_BUDGET_MS,
createProjectAutoDiscover, projectSyncDelay, store, type ProjectSyncEnv, type ProjectSyncStatus,
} from './store'
import type { Conversation, Project } from './contracts'
import type { ConversationView } from './api'
import type { Project } from './contracts'
const project = (id: string, owner = 'alice'): Project => ({ id, owner, name: id, createdAt: '', updatedAt: '' })
const task = (id: string, projectId: string): Conversation => ({ id, projectId, title: id, createdAt: '', updatedAt: '' })
const task = (id: string, projectId: string): ConversationView => ({ id, projectId, title: id, createdAt: '', updatedAt: '' })
const response = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status })
function deferred<T>() {
let resolve!: (value: T) => void
@@ -39,7 +40,7 @@ function fakeEnv() {
}
let projects: Project[]
let tasks: Conversation[]
let tasks: ConversationView[]
let requests: { path: string; init?: RequestInit }[]
let route: (path: string, init?: RequestInit) => Promise<Response>
+4 -5
View File
@@ -1,7 +1,7 @@
// 应用状态:服务端权威(事件 + Query 对账),前端 store 是投影。
// useSyncExternalStore 驱动;live 流式部件为会话内临时态,run 终态后由消息树对账收敛。
import { useSyncExternalStore } from 'react'
import { type AdminZoneSection, type LinkedCapabilities, api, connectEvents } from './api'
import { type AdminZoneSection, type ConversationView, type LinkedCapabilities, api, connectEvents } from './api'
import { auth } from './auth'
import { noProjectError, projectActionError, projectWorkError } from './projectmeta'
import { applyTextDelta, applyThinkingDelta, applyToolCall, applyToolCallDelta, applyToolResult, applyToolOutput, applyArtifactCreated, applyArtifactUpdated, applyCheck, applyFinalizing, type LiveItem } from './live'
@@ -26,7 +26,6 @@ import type {
TaskSourceRef,
AgentProfile,
Artifact,
Conversation,
HITLCard,
Message,
PlanSnapshot,
@@ -116,7 +115,7 @@ interface AppState {
currentProjectId: string | null
/** 无项目对话视图:currentProjectId 此时是无项目对话的内部项目,不在 projects 里。 */
scratchMode: boolean
tasks: Conversation[]
tasks: ConversationView[]
me: Me | null
currentConvId: string | null
messages: Message[]
@@ -379,9 +378,9 @@ async function bindProject(projectId: string, remember: boolean) {
await store.refreshArtifacts(projectId)
}
function latestScratchTask(): Conversation | undefined {
function latestScratchTask(): ConversationView | undefined {
return state.tasks.filter(t => t.scratch && !t.archived)
.reduce<Conversation | undefined>((latest, t) => !latest || t.updatedAt > latest.updatedAt ? t : latest, undefined)
.reduce<ConversationView | undefined>((latest, t) => !latest || t.updatedAt > latest.updatedAt ? t : latest, undefined)
}
function observeDetail(observations: Record<string, DetailObservation>, id: string, seq = state.lastSeq, terminal = false) {
+5 -4
View File
@@ -1,16 +1,17 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { auth } from '../src/auth'
import { canCreateProject, store } from '../src/store'
import type { Conversation, Project } from '../src/contracts'
import type { ConversationView } from '../src/api'
import type { Project } from '../src/contracts'
const project = (id: string): Project => ({ id, name: id, createdAt: '', updatedAt: '' })
const task = (id: string, projectId: string, scratch = false, updatedAt = ''): Conversation =>
const task = (id: string, projectId: string, scratch = false, updatedAt = ''): ConversationView =>
({ id, projectId, title: id, createdAt: '', updatedAt, ...(scratch ? { scratch: true } : {}) })
const response = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status })
const detail = (conversation: Conversation) => response({ conversation, messages: [], runs: [], activeHitl: [], lastSeq: 0 })
const detail = (conversation: ConversationView) => response({ conversation, messages: [], runs: [], activeHitl: [], lastSeq: 0 })
let projects: Project[]
let tasks: Conversation[]
let tasks: ConversationView[]
let me: Record<string, unknown>
let requests: { path: string; init?: RequestInit }[]
let scratchCount: number