Compare commits
4
Commits
4882e0ae25
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac5cf6b322 | ||
|
|
a82a1e18de | ||
|
|
279b2d02a2 | ||
|
|
6e611c5de0 |
+6
-4
@@ -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,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?.() }}>
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user