diff --git a/src/api.ts b/src/api.ts index 30af7b7..5fe8429 100644 --- a/src/api.ts +++ b/src/api.ts @@ -28,6 +28,8 @@ export const api = { listTasks: (includeArchived?: boolean) => auth.fetch(`${BASE}/api/conversations${includeArchived ? '?archived=1' : ''}`).then(r => json(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)), + rewindFiles: (id: string, messageId: string) => auth.fetch(`${BASE}/api/conversations/${id}/rewind`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messageId }) }).then(r => json<{ restored?: string[]; skipped?: string[] }>(r)), me: () => auth.fetch(`${BASE}/api/me`).then(r => json<{ owner: string; name: string; auth: boolean; isAdmin?: boolean; mustChangePassword?: boolean }>(r)), // ---- 认证与会话(管理员发放制,R2)---- login: (owner: string, password: string) => auth.fetch(`${BASE}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ owner, password }) }).then(async r => { diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 3103106..7e33872 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -540,6 +540,7 @@ export function Conversation() { 你
{m.parts.filter(p => p.type === 'text').map(p => p.text).join('\n')}
+ {currentConvId && } {anchored.map(c => ( store.cardAction(card, actionId), @@ -616,3 +617,63 @@ export function Conversation() { ) } + +function RewindControl({ convId, messageId }: { convId: string; messageId: string }) { + const [open, setOpen] = useState(false) + const [files, setFiles] = useState<{ restored: string[]; skipped: string[] } | null>(null) + const [busy, setBusy] = useState(false) + const [err, setErr] = useState('') + async function preview() { + setErr('') + setOpen(true) + setFiles(null) + try { + const plan = await api.checkpoints(convId, messageId) + setFiles({ restored: plan.restored ?? [], skipped: plan.skipped ?? [] }) + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)) + } + } + async function confirm() { + setBusy(true) + setErr('') + try { + await api.rewindFiles(convId, messageId) + setOpen(false) + await store.openTask(convId) + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)) + } finally { + setBusy(false) + } + } + const canRewind = !!files && files.restored.length > 0 + return ( +
+ + {open && ( +
+ {!files && !err &&
正在列出文件…
} + {files && files.restored.length === 0 && files.skipped.length === 0 &&
没有需要回滚的文件。
} + {files && files.restored.length > 0 && ( + <> +
将恢复这些文件:
+
    {files.restored.map(p =>
  • {p}
  • )}
+ + )} + {files && files.skipped.length > 0 && ( + <> +
这些文件在追踪之外被改过,会跳过:
+
    {files.skipped.map(p =>
  • {p}
  • )}
+ + )} + {err &&
{err}
} +
+ + +
+
+ )} +
+ ) +} diff --git a/src/contracts.ts b/src/contracts.ts index 64044d6..43046eb 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -55,6 +55,7 @@ export type RunPhase = 'thinking' | 'acting' | 'responding' | 'finalizing' | 'do export interface Usage { promptTokens: number; completionTokens: number; totalTokens: number; durationMs: number; toolCalls: number; + model?: string; } export interface Run { diff --git a/src/styles.css b/src/styles.css index 483a035..746291a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -553,3 +553,12 @@ button { font-family: inherit; font-size: inherit; color: inherit; } margin-left: 6px; min-width: 16px; padding: 0 5px; border-radius: var(--radius-full); background: var(--err); color: var(--text-on-accent); font-size: var(--fs-ui-xs); } +.rewind-row { margin-top: 8px; } +.rewind-panel { + margin-top: 8px; padding: 8px 10px; + background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-sm); + font-size: var(--fs-ui-sm); +} +.rewind-panel ul { margin: 6px 0; padding-left: 1.2em; } +.rewind-actions { display: flex; gap: 8px; } +.rewind-error { color: var(--err); font-size: var(--fs-ui-sm); }