(null);
- const markScopeRestored = useGlobalStore((s) => s.markScopeRestored);
const markFileRestored = useGlobalStore((s) => s.markFileRestored);
const setPendingInput = useGlobalStore((s) => s.setPendingInput);
@@ -420,20 +406,19 @@ export default function MessageStream({ threadId }: MessageStreamProps) {
[threadId, revertFile, undoCodeRollback, markFileRestored]
);
- const handleRevertScope = useCallback(
- async (uiTurnId: string, scope: 'agent' | 'all', isReverted: boolean) => {
+ const handleRevertTurn = useCallback(
+ async (uiTurnId: string, files: string[], isReverted: boolean) => {
if (isReverted) {
const result = await undoCodeRollback(threadId, uiTurnId, false);
if (result.restored) {
- markScopeRestored(threadId, uiTurnId, scope);
+ const key = `${threadId}:${uiTurnId}`;
+ delete useGlobalStore.getState().rollback.revertedFilesByTurnId[key];
}
- } else if (scope === 'agent') {
- await revertAgentFiles(threadId);
} else {
- await revertAllFiles(threadId);
+ await revertFiles(threadId, files);
}
},
- [threadId, revertAgentFiles, revertAllFiles, undoCodeRollback, markScopeRestored]
+ [threadId, revertFiles, undoCodeRollback]
);
const rollbackModal = showRollbackPanel && (
@@ -573,7 +558,7 @@ export default function MessageStream({ threadId }: MessageStreamProps) {
isInterrupted={isInterrupted}
threadId={threadId}
onRevertFile={handleRevertFile}
- onRevertScope={handleRevertScope}
+ onRevertTurn={handleRevertTurn}
/>
)}
diff --git a/packages/desktop/src/hooks/useAgent.ts b/packages/desktop/src/hooks/useAgent.ts
index bc2f6b3..5857ac5 100644
--- a/packages/desktop/src/hooks/useAgent.ts
+++ b/packages/desktop/src/hooks/useAgent.ts
@@ -11,10 +11,7 @@ import {
deleteSession,
sendApprovalResponse,
getCheckpointDiff,
- revertCheckpointFile,
revertCheckpointFiles,
- revertCheckpointAgentFiles,
- revertCheckpointAllFiles,
previewRollbackDiff,
rollbackCodeToTurn,
rollbackContext,
@@ -362,7 +359,6 @@ export function useAgentRollback() {
const setRollbackPreview = useGlobalStore((s) => s.setRollbackPreview);
const markFileReverted = useGlobalStore((s) => s.markFileReverted);
const markFileRestored = useGlobalStore((s) => s.markFileRestored);
- const markScopeReverted = useGlobalStore((s) => s.markScopeReverted);
const setTurnCheckpointMapping = useGlobalStore((s) => s.setTurnCheckpointMapping);
const initRevertedFilesFromState = useGlobalStore((s) => s.initRevertedFilesFromState);
@@ -398,7 +394,7 @@ export function useAgentRollback() {
const revertFile = useCallback(
async (threadId: string, file: string) => {
const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
- const { result } = await revertCheckpointFile(threadId, cwd, file);
+ const { result } = await revertCheckpointFiles(threadId, cwd, [file]);
if (result.reverted) {
markFileReverted(threadId, resolveUITurnId(threadId, result.throughTurnId), file);
}
@@ -422,30 +418,6 @@ export function useAgentRollback() {
[workspace.rootPath, markFileReverted, resolveUITurnId]
);
- const revertAgentFiles = useCallback(
- async (threadId: string) => {
- const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
- const { result } = await revertCheckpointAgentFiles(threadId, cwd);
- if (result.reverted) {
- markScopeReverted(threadId, resolveUITurnId(threadId, result.throughTurnId), 'agent');
- }
- return result;
- },
- [workspace.rootPath, markScopeReverted, resolveUITurnId]
- );
-
- const revertAllFiles = useCallback(
- async (threadId: string) => {
- const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
- const { result } = await revertCheckpointAllFiles(threadId, cwd);
- if (result.reverted) {
- markScopeReverted(threadId, resolveUITurnId(threadId, result.throughTurnId), 'all');
- }
- return result;
- },
- [workspace.rootPath, markScopeReverted, resolveUITurnId]
- );
-
const previewRollback = useCallback(
async (threadId: string, throughTurnId: number) => {
const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
@@ -585,8 +557,6 @@ export function useAgentRollback() {
loadCheckpointDiff,
revertFile,
revertFiles,
- revertAgentFiles,
- revertAllFiles,
previewRollback,
rollbackCode,
rollbackCtx,
diff --git a/packages/desktop/src/lib/core-api.ts b/packages/desktop/src/lib/core-api.ts
index f36454a..804f04c 100644
--- a/packages/desktop/src/lib/core-api.ts
+++ b/packages/desktop/src/lib/core-api.ts
@@ -223,7 +223,6 @@ export interface CheckpointDiff {
turnId: number;
files: Array<{
path: string;
- source: 'agent' | 'unknown';
status: string;
diff: string;
insertions: number;
@@ -234,7 +233,6 @@ export interface CheckpointDiff {
export interface CodeRollbackResult {
reverted: boolean;
throughTurnId: number;
- baseTurnId: number | null;
affectedTurns: number[];
selectedFiles: string[];
restoreEntry: CodeRestoreEntry | null;
@@ -250,7 +248,6 @@ export interface CodeRollbackUndoResult {
export interface RollbackPreviewDiff {
throughTurnId: number;
- baseTurnId: number | null;
affectedTurns: number[];
diff: string;
}
@@ -260,7 +257,6 @@ export interface CodeRestoreEntry {
sessionId: string;
action: string;
throughTurnId: number;
- baseTurnId: number;
affectedTurns: number[];
selectedFiles: string[];
safetyCommit: string;
@@ -285,14 +281,6 @@ export function getCheckpointDiff(
return clients.sessions.getCheckpointDiff({ sessionId, cwd, turnId }) as any;
}
-export function revertCheckpointFile(
- sessionId: string,
- cwd: string,
- file: string
-): Promise<{ ok: boolean; result: CodeRollbackResult }> {
- return clients.sessions.revertCheckpointFile({ sessionId, cwd, file }) as any;
-}
-
export function revertCheckpointFiles(
sessionId: string,
cwd: string,
@@ -301,20 +289,6 @@ export function revertCheckpointFiles(
return clients.sessions.revertCheckpointFiles({ sessionId, cwd, files }) as any;
}
-export function revertCheckpointAgentFiles(
- sessionId: string,
- cwd: string
-): Promise<{ ok: boolean; result: CodeRollbackResult }> {
- return clients.sessions.revertCheckpointAgentFiles({ sessionId, cwd }) as any;
-}
-
-export function revertCheckpointAllFiles(
- sessionId: string,
- cwd: string
-): Promise<{ ok: boolean; result: CodeRollbackResult }> {
- return clients.sessions.revertCheckpointAllFiles({ sessionId, cwd }) as any;
-}
-
export function previewRollbackDiff(
sessionId: string,
cwd: string,
diff --git a/packages/desktop/src/stores/global.store.ts b/packages/desktop/src/stores/global.store.ts
index f847b99..cb6a708 100644
--- a/packages/desktop/src/stores/global.store.ts
+++ b/packages/desktop/src/stores/global.store.ts
@@ -169,8 +169,6 @@ interface GlobalActions {
clearRollbackPreview: (threadId: string) => void;
markFileReverted: (threadId: string, turnId: string, file: string) => void;
markFileRestored: (threadId: string, turnId: string, file: string) => void;
- markScopeReverted: (threadId: string, turnId: string, scope: 'agent' | 'all') => void;
- markScopeRestored: (threadId: string, turnId: string, scope: 'agent' | 'all') => void;
initRevertedFilesFromState: (threadId: string) => void;
setTurnCheckpointMapping: (threadId: string, checkpointId: number, uiTurnId: string) => void;
startCompressing: () => void;
@@ -677,31 +675,6 @@ export const useGlobalStore = create
()(
s.rollback.revertedFilesByTurnId[key] = arr.filter((f) => f !== file);
}
}),
- markScopeReverted: (threadId, turnId, scope) =>
- set((s) => {
- const key = `${threadId}:${turnId}`;
- const sentinel =
- scope === 'agent' ? '__scope_agent_reverted__' : '__scope_all_reverted__';
- if (!s.rollback.revertedFilesByTurnId[key]) {
- s.rollback.revertedFilesByTurnId[key] = [];
- }
- if (!s.rollback.revertedFilesByTurnId[key].includes(sentinel)) {
- s.rollback.revertedFilesByTurnId[key].push(sentinel);
- }
- }),
- markScopeRestored: (threadId, turnId, scope) =>
- set((s) => {
- const key = `${threadId}:${turnId}`;
- const sentinel =
- scope === 'agent' ? '__scope_agent_reverted__' : '__scope_all_reverted__';
- const arr = s.rollback.revertedFilesByTurnId[key];
- if (arr) {
- s.rollback.revertedFilesByTurnId[key] = arr.filter((f) => f !== sentinel);
- if (s.rollback.revertedFilesByTurnId[key].length === 0) {
- delete s.rollback.revertedFilesByTurnId[key];
- }
- }
- }),
initRevertedFilesFromState: (threadId) =>
set((s) => {
const state = s.rollback.rollbackStateByThreadId[threadId];
diff --git a/packages/desktop/test/global-store-rollback-state.test.ts b/packages/desktop/test/global-store-rollback-state.test.ts
index 1daed60..d6022c6 100644
--- a/packages/desktop/test/global-store-rollback-state.test.ts
+++ b/packages/desktop/test/global-store-rollback-state.test.ts
@@ -38,7 +38,7 @@ describe('Rollback state in global store', () => {
files: [
{
path: '/test/a.ts',
- source: 'agent' as const,
+
status: 'M',
diff: '---\n+++\n',
insertions: 2,
@@ -59,7 +59,6 @@ describe('Rollback state in global store', () => {
it('setRollbackPreview stores preview', () => {
const preview = {
throughTurnId: 2,
- baseTurnId: 1,
affectedTurns: [3, 4],
diff: 'diff content',
};
@@ -73,7 +72,6 @@ describe('Rollback state in global store', () => {
it('clearRollbackPreview removes preview', () => {
const preview = {
throughTurnId: 2,
- baseTurnId: 1,
affectedTurns: [3, 4],
diff: 'diff content',
};
@@ -106,35 +104,6 @@ describe('Rollback state in global store', () => {
expect(reverted).toEqual(['/test/b.ts']);
});
- it('markScopeReverted sets sentinel', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- const reverted = useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3'];
- expect(reverted).toContain('__scope_agent_reverted__');
- });
-
- it('markScopeReverted differentiates agent and all scopes', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'all');
- const reverted = useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3'];
- expect(reverted).toContain('__scope_agent_reverted__');
- expect(reverted).toContain('__scope_all_reverted__');
- });
-
- it('markScopeRestored removes correct sentinel', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'all');
- useGlobalStore.getState().markScopeRestored('thread1', '3', 'agent');
- const reverted = useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3'];
- expect(reverted).not.toContain('__scope_agent_reverted__');
- expect(reverted).toContain('__scope_all_reverted__');
- });
-
- it('markScopeRestored removes entry when empty', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- useGlobalStore.getState().markScopeRestored('thread1', '3', 'agent');
- expect(useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3']).toBeUndefined();
- });
-
it('initRevertedFilesFromState populates from server state', () => {
useGlobalStore.getState().setTurnCheckpointMapping('thread1', 5, 'ui-turn-5');
const state = {
@@ -167,7 +136,7 @@ describe('Rollback state in global store', () => {
files: [
{
path: '/a.ts',
- source: 'agent' as const,
+
status: 'M',
diff: '---\n+++\n',
insertions: 1,
@@ -188,7 +157,7 @@ describe('Rollback state in global store', () => {
files: [
{
path: '/b.ts',
- source: 'agent' as const,
+
status: 'M',
diff: '---\n+++\n',
insertions: 1,
diff --git a/packages/infra/package.json b/packages/infra/package.json
index 76715a1..37a9517 100644
--- a/packages/infra/package.json
+++ b/packages/infra/package.json
@@ -5,7 +5,8 @@
"main": "./src/config.ts",
"exports": {
"./config": "./src/config.ts",
- "./logger": "./src/logger.ts"
+ "./logger": "./src/logger.ts",
+ "./disabled-store": "./src/disabled-store.ts"
},
"dependencies": {
"pino": "^9.6.0",
diff --git a/packages/infra/src/disabled-store.ts b/packages/infra/src/disabled-store.ts
new file mode 100644
index 0000000..a334664
--- /dev/null
+++ b/packages/infra/src/disabled-store.ts
@@ -0,0 +1,123 @@
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
+import { dirname, join } from 'path';
+import { homedir } from 'os';
+import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
+
+export interface DisabledStoreConfig {
+ globalKeyPath: string[];
+ /** Optional function that returns the global config directory. Defaults to ~/.codingcode */
+ getGlobalConfigDir?: () => string;
+}
+
+export interface DisabledStore {
+ getGlobal(name: string): boolean;
+ setGlobal(name: string, disabled: boolean): void;
+ getProject(projectRoot: string, name: string): boolean | undefined;
+ setProject(projectRoot: string, name: string, disabled: boolean): void;
+ resetProject(projectRoot: string, name: string): void;
+ resolve(projectRoot: string, name: string): boolean;
+}
+
+function deepSet(
+ obj: Record,
+ path: string[],
+ name: string,
+ value: unknown
+): void {
+ let target: any = obj;
+ for (let i = 0; i < path.length - 1; i++) {
+ const key = path[i]!;
+ if (!target[key]) target[key] = {};
+ target = target[key];
+ if (!target) return;
+ }
+ const lastKey = path[path.length - 1]!;
+ const map = (target[lastKey] as Record) ?? {};
+ map[name] = value;
+ target[lastKey] = map;
+}
+
+function deepGet(obj: any, path: string[]): any {
+ let value = obj;
+ for (const k of path) {
+ value = value?.[k];
+ }
+ return value;
+}
+
+function deepDelete(obj: any, path: string[], name: string): void {
+ const value = deepGet(obj, path);
+ if (value && typeof value === 'object') {
+ delete (value as Record)[name];
+ }
+}
+
+export function createDisabledStore(cfg: DisabledStoreConfig): DisabledStore {
+ const globalConfigPath = () =>
+ join(cfg.getGlobalConfigDir?.() ?? join(homedir(), '.codingcode'), 'config.yaml');
+
+ const getGlobal = (name: string): boolean => {
+ const p = globalConfigPath();
+ if (!existsSync(p)) return false;
+ try {
+ const config = parseYaml(readFileSync(p, 'utf8')) as any;
+ const value = deepGet(config, cfg.globalKeyPath);
+ return (value as Record)?.[name] ?? false;
+ } catch {
+ return false;
+ }
+ };
+
+ const setGlobal = (name: string, disabled: boolean): void => {
+ const p = globalConfigPath();
+ const dir = dirname(p);
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
+ const existing: Record = existsSync(p)
+ ? (parseYaml(readFileSync(p, 'utf8')) as Record)
+ : {};
+ deepSet(existing, cfg.globalKeyPath, name, disabled);
+ writeFileSync(p, stringifyYaml(existing), 'utf8');
+ };
+
+ const getProject = (projectRoot: string, name: string): boolean | undefined => {
+ const p = join(projectRoot, '.codingcode', 'config.yaml');
+ if (!existsSync(p)) return undefined;
+ try {
+ const config = parseYaml(readFileSync(p, 'utf8')) as Record;
+ const value = deepGet(config, cfg.globalKeyPath);
+ return (value as Record)?.[name];
+ } catch {
+ return undefined;
+ }
+ };
+
+ const setProject = (projectRoot: string, name: string, disabled: boolean): void => {
+ const dir = join(projectRoot, '.codingcode');
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
+ const p = join(dir, 'config.yaml');
+ const existing: Record = existsSync(p)
+ ? (parseYaml(readFileSync(p, 'utf8')) as Record)
+ : {};
+ deepSet(existing, cfg.globalKeyPath, name, disabled);
+ writeFileSync(p, stringifyYaml(existing), 'utf8');
+ };
+
+ const resetProject = (projectRoot: string, name: string): void => {
+ const p = join(projectRoot, '.codingcode', 'config.yaml');
+ if (!existsSync(p)) return;
+ const existing: Record = parseYaml(readFileSync(p, 'utf8')) as Record<
+ string,
+ unknown
+ >;
+ deepDelete(existing, cfg.globalKeyPath, name);
+ writeFileSync(p, stringifyYaml(existing), 'utf8');
+ };
+
+ const resolve = (projectRoot: string, name: string): boolean => {
+ const pv = getProject(projectRoot, name);
+ if (pv !== undefined) return pv;
+ return getGlobal(name);
+ };
+
+ return { getGlobal, setGlobal, getProject, setProject, resetProject, resolve };
+}
diff --git a/packages/tui/src/commands/registry.ts b/packages/tui/src/commands/registry.ts
index ea60215..bece142 100644
--- a/packages/tui/src/commands/registry.ts
+++ b/packages/tui/src/commands/registry.ts
@@ -14,12 +14,6 @@ export const COMMAND_REGISTRY = {
usage: '/sessions',
title: '恢复会话',
},
- checkpoint: {
- name: 'checkpoint',
- description: '管理文件快照,回退/前进',
- usage: '/checkpoint',
- title: '检查点',
- },
help: { name: 'help', description: '显示帮助', usage: '/help', title: '帮助' },
clear: {
name: 'clear',
diff --git a/packages/tui/src/components/App.tsx b/packages/tui/src/components/App.tsx
index acacf9e..470c328 100644
--- a/packages/tui/src/components/App.tsx
+++ b/packages/tui/src/components/App.tsx
@@ -254,24 +254,6 @@ export function App({ client }: AppProps) {
}
return;
}
- if (parsed.name === 'checkpoint') {
- try {
- const checkpoints = await client.getCheckpoints();
- setPanel({ type: 'checkpoint-list', checkpoints });
- } catch (e: any) {
- setStaticMessages((prev) => [
- ...prev,
- {
- id: generateId(),
- timestamp: Date.now(),
- role: 'system' as const,
- content: `[Checkpoint Error] ${e.message || e}`,
- },
- ]);
- return;
- }
- return;
- }
if (parsed.name === 'help') {
setPanel({ type: 'help' });
return;
@@ -426,57 +408,6 @@ export function App({ client }: AppProps) {
width={sessionW}
/>
)}
- {panel.type === 'checkpoint-list' && (
- ({
- label: `${cp.title || '(无标题)'} ${cp.agentModified.length + cp.unknownSource.length} 个文件`,
- value: cp.turnId.toString(),
- }))
- }
- onSelect={async (value) => {
- const cp = panel.checkpoints.find((c) => c.turnId.toString() === value);
- if (!cp) return;
- const hasForward = await client.hasForwardStack();
- setPanel({ type: 'checkpoint-action', cp, hasForward });
- }}
- onCancel={() => setPanel({ type: 'none' })}
- width={Math.min(60, width - 4)}
- />
- )}
- {panel.type === 'checkpoint-action' && (
- 0
- ? [
- {
- label: `仅回退 Agent 修改的文件 (${panel.cp.agentModified.length} 个)`,
- value: 'agent' as const,
- },
- {
- label: `回退全部文件 (${panel.cp.agentModified.length + panel.cp.unknownSource.length} 个)`,
- value: 'all' as const,
- },
- ]
- : [{ label: '无变更文件', value: '' as const }]),
- ...(panel.hasForward ? [{ label: '前进到最新状态', value: 'forward' as const }] : []),
- ]}
- onSelect={async (value) => {
- if (value === 'forward') {
- await client.forwardLastRevert();
- } else if (value === 'agent' || value === 'all') {
- await client.revertCheckpoint(panel.cp.turnId, value);
- }
- setPanel({ type: 'none' });
- }}
- onCancel={() => setPanel({ type: 'none' })}
- width={Math.min(60, width - 4)}
- />
- )}
{approval && (
{
description?: string;
}
-export interface CheckpointInfo {
- turnId: number;
- title: string;
- agentModified: string[];
- unknownSource: string[];
-}
-
export interface McpServerStatus {
name: string;
connected: boolean;
@@ -51,8 +44,6 @@ export type PanelState =
| { type: 'model'; items: PanelItem[]; activeValue: string }
| { type: 'sessions'; items: PanelItem[] }
| { type: 'approval'; id: string; tool: string; args: Record }
- | { type: 'checkpoint-list'; checkpoints: CheckpointInfo[] }
- | { type: 'checkpoint-action'; cp: CheckpointInfo; hasForward: boolean }
| { type: 'help' }
| { type: 'mcp'; servers: McpServerStatus[] }
| { type: 'skill'; skills: SkillStatus[] }