|
@@ -19,6 +19,11 @@ import {
|
|
|
import { processImageAttachments } from './hooks/image-hook';
|
|
import { processImageAttachments } from './hooks/image-hook';
|
|
|
import { createInterviewManager } from './interview';
|
|
import { createInterviewManager } from './interview';
|
|
|
import { createBuiltinMcps } from './mcp';
|
|
import { createBuiltinMcps } from './mcp';
|
|
|
|
|
+import {
|
|
|
|
|
+ createCheckpointCommands,
|
|
|
|
|
+ createCheckpointManager,
|
|
|
|
|
+ createCheckpointTools,
|
|
|
|
|
+} from './checkpoints';
|
|
|
import {
|
|
import {
|
|
|
getMultiplexer,
|
|
getMultiplexer,
|
|
|
MultiplexerSessionManager,
|
|
MultiplexerSessionManager,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
createCouncilTool,
|
|
createCouncilTool,
|
|
|
createWebfetchTool,
|
|
createWebfetchTool,
|
|
|
} from './tools';
|
|
} from './tools';
|
|
|
|
|
+import type { CheckpointManager } from './checkpoints';
|
|
|
import { resolveRuntimeAgentName, rewriteDisplayNameMentions } from './utils';
|
|
import { resolveRuntimeAgentName, rewriteDisplayNameMentions } from './utils';
|
|
|
import { initLogger, log } from './utils/logger';
|
|
import { initLogger, log } from './utils/logger';
|
|
|
import { SubagentDepthTracker } from './utils/subagent-depth';
|
|
import { SubagentDepthTracker } from './utils/subagent-depth';
|
|
@@ -250,9 +256,18 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
});
|
|
});
|
|
|
interviewManager = createInterviewManager(ctx, config);
|
|
interviewManager = createInterviewManager(ctx, config);
|
|
|
|
|
|
|
|
|
|
+ // Initialize checkpoint manager for session rollback
|
|
|
|
|
+ const checkpointManager = createCheckpointManager(ctx);
|
|
|
|
|
+ const checkpointCommands = createCheckpointCommands(checkpointManager);
|
|
|
|
|
+
|
|
|
|
|
+ // Get current session ID for tools
|
|
|
|
|
+ let currentSessionID: string | undefined;
|
|
|
|
|
+ const checkpointTools = createCheckpointTools(ctx, checkpointManager, () => currentSessionID);
|
|
|
|
|
+
|
|
|
toolCount =
|
|
toolCount =
|
|
|
Object.keys(councilTools).length +
|
|
Object.keys(councilTools).length +
|
|
|
Object.keys(todoContinuationHook.tool).length +
|
|
Object.keys(todoContinuationHook.tool).length +
|
|
|
|
|
+ Object.keys(checkpointTools).length +
|
|
|
1 + // webfetch
|
|
1 + // webfetch
|
|
|
2; // ast_grep_search, ast_grep_replace
|
|
2; // ast_grep_search, ast_grep_replace
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
@@ -320,6 +335,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
...councilTools,
|
|
...councilTools,
|
|
|
webfetch,
|
|
webfetch,
|
|
|
...todoContinuationHook.tool,
|
|
...todoContinuationHook.tool,
|
|
|
|
|
+ ...checkpointTools,
|
|
|
ast_grep_search,
|
|
ast_grep_search,
|
|
|
ast_grep_replace,
|
|
ast_grep_replace,
|
|
|
},
|
|
},
|
|
@@ -520,6 +536,30 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interviewManager.registerCommand(opencodeConfig);
|
|
interviewManager.registerCommand(opencodeConfig);
|
|
|
|
|
+
|
|
|
|
|
+ // Register checkpoint commands
|
|
|
|
|
+ const configCommand = opencodeConfig.command as
|
|
|
|
|
+ | Record<string, unknown>
|
|
|
|
|
+ | undefined;
|
|
|
|
|
+ if (!configCommand?.['checkpoint']) {
|
|
|
|
|
+ if (!opencodeConfig.command) {
|
|
|
|
|
+ opencodeConfig.command = {};
|
|
|
|
|
+ }
|
|
|
|
|
+ (opencodeConfig.command as Record<string, unknown>)['checkpoint'] = {
|
|
|
|
|
+ template: 'checkpoint <subcommand> [args]',
|
|
|
|
|
+ description:
|
|
|
|
|
+ 'Save and restore session checkpoints — /checkpoint save <name>, /checkpoint list, /checkpoint delete <name>',
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!configCommand?.['rollback']) {
|
|
|
|
|
+ if (!opencodeConfig.command) {
|
|
|
|
|
+ opencodeConfig.command = {};
|
|
|
|
|
+ }
|
|
|
|
|
+ (opencodeConfig.command as Record<string, unknown>)['rollback'] = {
|
|
|
|
|
+ template: 'rollback <name>',
|
|
|
|
|
+ description: 'Rollback to a saved checkpoint',
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
event: async (input) => {
|
|
event: async (input) => {
|
|
@@ -588,6 +628,19 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
if (sessionID) {
|
|
if (sessionID) {
|
|
|
sessionAgentMap.delete(sessionID);
|
|
sessionAgentMap.delete(sessionID);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Cleanup checkpoints for deleted session
|
|
|
|
|
+ if (sessionID && checkpointManager) {
|
|
|
|
|
+ await checkpointManager.cleanupSession(sessionID);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Track current session ID for checkpoint tools
|
|
|
|
|
+ if (event.type === 'session.status' || event.type === 'session.created') {
|
|
|
|
|
+ const sid = event.properties?.sessionID ?? event.properties?.info?.id;
|
|
|
|
|
+ if (sid) {
|
|
|
|
|
+ currentSessionID = sid;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -608,23 +661,65 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
// Direct interception of /auto-continue command — bypasses LLM
|
|
// Direct interception of /auto-continue command — bypasses LLM
|
|
|
// round-trip
|
|
// round-trip
|
|
|
'command.execute.before': async (input, output) => {
|
|
'command.execute.before': async (input, output) => {
|
|
|
- await todoContinuationHook.handleCommandExecuteBefore(
|
|
|
|
|
- input as {
|
|
|
|
|
- command: string;
|
|
|
|
|
- sessionID: string;
|
|
|
|
|
- arguments: string;
|
|
|
|
|
- },
|
|
|
|
|
- output as { parts: Array<{ type: string; text?: string }> },
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ const cmdInput = input as {
|
|
|
|
|
+ command: string;
|
|
|
|
|
+ sessionID: string;
|
|
|
|
|
+ arguments: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ const cmdOutput = output as { parts: Array<{ type: string; text?: string }> };
|
|
|
|
|
+
|
|
|
|
|
+ await todoContinuationHook.handleCommandExecuteBefore(cmdInput, cmdOutput);
|
|
|
|
|
+
|
|
|
|
|
+ await interviewManager.handleCommandExecuteBefore(cmdInput, cmdOutput);
|
|
|
|
|
+
|
|
|
|
|
+ // Handle checkpoint commands
|
|
|
|
|
+ if (cmdInput.command === 'checkpoint') {
|
|
|
|
|
+ const args = cmdInput.arguments.trim();
|
|
|
|
|
+ const subcommandMatch = args.match(/^(\S+)(?:\s+(.*))?$/);
|
|
|
|
|
+ if (subcommandMatch) {
|
|
|
|
|
+ const [, subcommand, rest] = subcommandMatch;
|
|
|
|
|
+ switch (subcommand) {
|
|
|
|
|
+ case 'save': {
|
|
|
|
|
+ const result = await checkpointCommands.handleCheckpointSave({
|
|
|
|
|
+ sessionID: cmdInput.sessionID,
|
|
|
|
|
+ arguments: rest || '',
|
|
|
|
|
+ });
|
|
|
|
|
+ cmdOutput.parts.push(...result.parts);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'list': {
|
|
|
|
|
+ const result = await checkpointCommands.handleCheckpointList({
|
|
|
|
|
+ sessionID: cmdInput.sessionID,
|
|
|
|
|
+ arguments: '',
|
|
|
|
|
+ });
|
|
|
|
|
+ cmdOutput.parts.push(...result.parts);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'delete': {
|
|
|
|
|
+ const result = await checkpointCommands.handleCheckpointDelete({
|
|
|
|
|
+ sessionID: cmdInput.sessionID,
|
|
|
|
|
+ arguments: rest || '',
|
|
|
|
|
+ });
|
|
|
|
|
+ cmdOutput.parts.push(...result.parts);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ default: {
|
|
|
|
|
+ cmdOutput.parts.push({
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: 'Unknown checkpoint subcommand. Use: save, list, or delete',
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- await interviewManager.handleCommandExecuteBefore(
|
|
|
|
|
- input as {
|
|
|
|
|
- command: string;
|
|
|
|
|
- sessionID: string;
|
|
|
|
|
- arguments: string;
|
|
|
|
|
- },
|
|
|
|
|
- output as { parts: Array<{ type: string; text?: string }> },
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ if (cmdInput.command === 'rollback') {
|
|
|
|
|
+ const result = await checkpointCommands.handleRollback({
|
|
|
|
|
+ sessionID: cmdInput.sessionID,
|
|
|
|
|
+ arguments: cmdInput.arguments,
|
|
|
|
|
+ });
|
|
|
|
|
+ cmdOutput.parts.push(...result.parts);
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
'chat.headers': chatHeadersHook['chat.headers'],
|
|
'chat.headers': chatHeadersHook['chat.headers'],
|