Add Codex clear and compact slash commands (#541)

* Add Codex clear and compact slash commands

* Stabilize queued thinking event test

* Interrupt active Codex turn before slash commands
This commit is contained in:
NightWatcher314
2026-04-29 14:15:44 +08:00
committed by GitHub
parent 0160da4bb5
commit a612be50d6
15 changed files with 460 additions and 16 deletions
+122
View File
@@ -16,6 +16,7 @@ import { AppServerEventConverter } from './utils/appServerEventConverter';
import { registerAppServerPermissionHandlers } from './utils/appServerPermissionAdapter';
import { buildThreadStartParams, buildTurnStartParams } from './utils/appServerConfig';
import { shouldIgnoreTerminalEvent } from './utils/terminalEventGuard';
import { parseCodexSpecialCommand } from './codexSpecialCommands';
import {
RemoteLauncherBase,
type RemoteLauncherDisplayContext,
@@ -609,6 +610,123 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
readyAfterTurnTimer.unref?.();
};
const sendVisibleStatus = (message: string) => {
messageBuffer.addMessage(message, 'status');
session.sendSessionEvent({ type: 'message', message });
};
const resetCurrentTurnState = () => {
turnInFlight = false;
allowAnonymousTerminalEvent = false;
this.currentTurnId = null;
permissionHandler.reset();
reasoningProcessor.abort();
diffProcessor.reset();
appServerEventConverter.reset();
session.onThinkingChange(false);
};
const interruptActiveTurn = async () => {
const threadId = this.currentThreadId;
const turnId = this.currentTurnId;
if (!threadId || !turnId) {
return;
}
try {
await appServerClient.interruptTurn({ threadId, turnId });
} catch (error) {
logger.debug('[Codex] Error interrupting app-server turn for slash command:', error);
}
};
const resumeExistingThreadForCompact = async (mode: EnhancedMode): Promise<string | null> => {
if (this.currentThreadId && this.currentThreadId !== invalidThreadId) {
hasThread = true;
return this.currentThreadId;
}
const resumeCandidate = session.sessionId && session.sessionId !== invalidThreadId
? session.sessionId
: null;
if (!resumeCandidate) {
return null;
}
const threadParams = buildThreadStartParams({
cwd: session.path,
mode,
mcpServers,
cliOverrides: session.codexCliOverrides
});
try {
const resumeResponse = await appServerClient.resumeThread({
threadId: resumeCandidate,
...threadParams
}, {
signal: this.abortController.signal
});
const resumeRecord = asRecord(resumeResponse);
const resumeThread = resumeRecord ? asRecord(resumeRecord.thread) : null;
const threadId = asString(resumeThread?.id) ?? resumeCandidate;
applyResolvedModel(resumeRecord?.model);
this.currentThreadId = threadId;
session.onSessionFound(threadId);
hasThread = true;
logger.debug(`[Codex] Resumed app-server thread ${threadId} for /compact`);
return threadId;
} catch (error) {
logger.warn(`[Codex] Failed to resume app-server thread ${resumeCandidate} for /compact`, error);
return null;
}
};
const handleSpecialCommand = async (message: QueuedMessage): Promise<boolean> => {
const specialCommand = parseCodexSpecialCommand(message.message);
if (!specialCommand.type) {
return false;
}
if (specialCommand.type === 'invalid') {
await interruptActiveTurn();
resetCurrentTurnState();
sendVisibleStatus(specialCommand.message);
return true;
}
if (specialCommand.type === 'clear') {
await interruptActiveTurn();
resetCurrentTurnState();
this.currentThreadId = null;
invalidThreadId = null;
hasThread = false;
session.resetCodexThread();
sendVisibleStatus('Context was reset');
return true;
}
await interruptActiveTurn();
resetCurrentTurnState();
const threadId = await resumeExistingThreadForCompact(message.mode);
if (!threadId) {
sendVisibleStatus('Nothing to compact');
return true;
}
sendVisibleStatus('Compaction started');
try {
await appServerClient.compactThread({ threadId }, {
signal: this.abortController.signal
});
sendVisibleStatus('Compaction completed');
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
sendVisibleStatus(`Compaction failed: ${detail}`);
}
return true;
};
while (!this.shouldExit) {
logActiveHandles('loop-top');
let message: QueuedMessage | null = pending;
@@ -634,6 +752,10 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
messageBuffer.addMessage(message.message, 'user');
try {
if (await handleSpecialCommand(message)) {
continue;
}
if (!hasThread) {
const threadParams = buildThreadStartParams({
cwd: session.path,