mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-12 00:42:06 +08:00
feat(chat): derive activity indicator from per-session state and unify provider lifecycle events
Replace the chat processing banner with a minimal activity indicator and
rebuild the state model underneath it. The old banner was driven by five
overlapping pieces of state (isLoading, canAbortSession, claudeStatus in the
chat, plus two app-level Sets updated in lockstep through four callbacks)
that had to be kept in sync imperatively. Because completion and status
events mutated the *viewed* session's flags regardless of which session they
belonged to, a background session finishing could hide the indicator for a
still-running session, returning to a finished session could briefly show a
stale banner, and a late status reply could override a newer request.
The fix is structural rather than patch-by-patch: a single
Map<sessionId, {statusText, canInterrupt, startedAt}> in useSessionProtection
is now the only source of truth for "this session is working". The indicator,
stop button, composer streaming state, and session protection are all derived
from the viewed session's entry on render, so there is no stale local copy to
restore or reset when switching sessions. A PENDING_SESSION_ID sentinel
covers the window before a new conversation receives its real session id.
Terminal events delete the entry atomically, which is why the indicator
disappears the instant the final chunk arrives. Stale check-session-status
replies are discarded via an ifStartedBefore guard (an idle reply older than
the entry's startedAt describes a previous request, not the current one).
The second half unifies the provider lifecycle contract, because the frontend
could not be made race-free while each provider terminated differently:
- cursor emitted complete twice per run (result line + process close), which
double-played the completion sound and let a late close-complete clear a
newer request's indicator
- aborts produced two completes (the abort-session reply plus the provider's
own non-aborted one), so cancelling a run played the celebration sound
- codex omitted exitCode; others attached ad-hoc fields (resultText, isError,
isNewSession) the client had to know about
- claude/codex failures ended with only an error event while gemini/cursor
also emit kind:'error' for mid-run stderr noise, so 'error' was ambiguous
between "the run died" and "a process wrote to stderr"
Every run now ends with exactly one complete built by createCompleteMessage()
({sessionId, actualSessionId, exitCode, success, aborted}); abort-session
sends it on behalf of cancelled runs and providers detect the abort and skip
their own. error is demoted to an informational row, so stderr noise no
longer kills the indicator mid-run, and the client celebrates only
success: true completes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,10 +28,14 @@ import {
|
||||
} from './services/notification-orchestrator.js';
|
||||
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
||||
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
||||
import { createNormalizedMessage } from './shared/utils.js';
|
||||
import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
|
||||
|
||||
const activeSessions = new Map();
|
||||
const pendingToolApprovals = new Map();
|
||||
// Sessions cancelled via abort-session. The abort handler already sent the
|
||||
// terminal `complete` (aborted: true) to the client, so the run loop must not
|
||||
// emit a second one when its generator winds down.
|
||||
const abortedSessionIds = new Set();
|
||||
|
||||
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
|
||||
|
||||
@@ -731,14 +735,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
||||
// Clean up temporary image files
|
||||
await cleanupTempFiles(tempImagePaths, tempDir);
|
||||
|
||||
// Send completion event
|
||||
ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 0, isNewSession: !sessionId && !!command, sessionId: capturedSessionId, provider: 'claude' }));
|
||||
// Send the terminal completion event — skipped for aborted runs, whose
|
||||
// terminal `complete` (aborted: true) was already sent by abort-session.
|
||||
const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
|
||||
if (!wasAborted) {
|
||||
ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
|
||||
}
|
||||
notifyRunStopped({
|
||||
userId: ws?.userId || null,
|
||||
provider: 'claude',
|
||||
sessionId: capturedSessionId || sessionId || null,
|
||||
sessionName: sessionSummary,
|
||||
stopReason: 'completed'
|
||||
stopReason: wasAborted ? 'aborted' : 'completed'
|
||||
});
|
||||
// Complete
|
||||
|
||||
@@ -753,14 +761,22 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
||||
// Clean up temporary image files on error
|
||||
await cleanupTempFiles(tempImagePaths, tempDir);
|
||||
|
||||
const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
|
||||
if (wasAborted) {
|
||||
// The abort already produced the terminal complete; a generator throw
|
||||
// caused by interrupt() is expected noise, not a user-facing error.
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Claude CLI is installed for a clearer error message
|
||||
const installed = await providerAuthService.isProviderInstalled('claude');
|
||||
const errorContent = !installed
|
||||
? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
|
||||
: error.message;
|
||||
|
||||
// Send error to WebSocket
|
||||
// Send error to WebSocket, then the terminal complete
|
||||
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
|
||||
ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
|
||||
notifyRunFailed({
|
||||
userId: ws?.userId || null,
|
||||
provider: 'claude',
|
||||
@@ -787,6 +803,10 @@ async function abortClaudeSDKSession(sessionId) {
|
||||
try {
|
||||
console.log(`Aborting SDK session: ${sessionId}`);
|
||||
|
||||
// Mark before interrupting so the run loop knows not to emit its own
|
||||
// terminal complete (the abort handler sends the aborted one).
|
||||
abortedSessionIds.add(sessionId);
|
||||
|
||||
// Call interrupt() on the query instance
|
||||
await session.instance.interrupt();
|
||||
|
||||
@@ -802,6 +822,8 @@ async function abortClaudeSDKSession(sessionId) {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error aborting session ${sessionId}:`, error);
|
||||
// The run keeps going; let it emit its own terminal complete.
|
||||
abortedSessionIds.delete(sessionId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { notifyRunFailed, notifyRunStopped } from './services/notification-orche
|
||||
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
||||
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
||||
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
||||
import { createNormalizedMessage } from './shared/utils.js';
|
||||
import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
|
||||
|
||||
// Use cross-spawn on Windows for better command execution
|
||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
||||
@@ -34,6 +34,10 @@ async function spawnCursor(command, options = {}, ws) {
|
||||
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
||||
let hasRetriedWithTrust = false;
|
||||
let settled = false;
|
||||
// The unified lifecycle contract requires exactly one terminal `complete`
|
||||
// per run. Cursor surfaces completion twice (the `result` JSON line and
|
||||
// the process close), so the first emission wins.
|
||||
let completeSent = false;
|
||||
|
||||
// Use tools settings passed from frontend, or defaults
|
||||
const settings = toolsSettings || {
|
||||
@@ -197,15 +201,15 @@ async function spawnCursor(command, options = {}, ws) {
|
||||
break;
|
||||
|
||||
case 'result': {
|
||||
// Session complete — send stream end + lifecycle complete with result payload
|
||||
const resultText = typeof response.result === 'string' ? response.result : '';
|
||||
ws.send(createNormalizedMessage({
|
||||
kind: 'complete',
|
||||
exitCode: response.subtype === 'success' ? 0 : 1,
|
||||
resultText,
|
||||
isError: response.subtype !== 'success',
|
||||
sessionId: capturedSessionId || sessionId, provider: 'cursor',
|
||||
}));
|
||||
// Session complete — terminal lifecycle event for this run
|
||||
if (!completeSent) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({
|
||||
provider: 'cursor',
|
||||
sessionId: capturedSessionId || sessionId || null,
|
||||
exitCode: response.subtype === 'success' ? 0 : 1,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -271,7 +275,12 @@ async function spawnCursor(command, options = {}, ws) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'cursor' }));
|
||||
// Terminal complete — unless the `result` line already sent it, or the
|
||||
// run was aborted (abort-session sent the aborted complete).
|
||||
if (!completeSent && !cursorProcess.aborted) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({ provider: 'cursor', sessionId: finalSessionId, exitCode: code }));
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
notifyTerminalState({ code });
|
||||
@@ -297,6 +306,10 @@ async function spawnCursor(command, options = {}, ws) {
|
||||
: error.message;
|
||||
|
||||
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
|
||||
if (!completeSent && !cursorProcess.aborted) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({ provider: 'cursor', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
|
||||
}
|
||||
notifyTerminalState({ error });
|
||||
|
||||
settleOnce(() => reject(error));
|
||||
@@ -314,6 +327,9 @@ function abortCursorSession(sessionId) {
|
||||
const process = activeCursorProcesses.get(sessionId);
|
||||
if (process) {
|
||||
console.log(`Aborting Cursor session: ${sessionId}`);
|
||||
// The abort handler sends the terminal complete (aborted: true); flag the
|
||||
// process so its close handler does not emit a second one.
|
||||
process.aborted = true;
|
||||
process.kill('SIGTERM');
|
||||
activeCursorProcesses.delete(sessionId);
|
||||
return true;
|
||||
|
||||
@@ -10,7 +10,7 @@ import GeminiResponseHandler from './gemini-response-handler.js';
|
||||
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
||||
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
||||
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
||||
import { createNormalizedMessage } from './shared/utils.js';
|
||||
import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
|
||||
|
||||
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
||||
@@ -129,6 +129,9 @@ async function spawnGemini(command, options = {}, ws) {
|
||||
let capturedSessionId = sessionId; // Track session ID throughout the process
|
||||
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
||||
let assistantBlocks = []; // Accumulate the full response blocks including tools
|
||||
// Unified lifecycle contract: exactly one terminal `complete` per run
|
||||
// (close and error handlers can both fire for spawn failures).
|
||||
let completeSent = false;
|
||||
|
||||
// Use tools settings passed from frontend, or defaults
|
||||
const settings = toolsSettings || {
|
||||
@@ -486,7 +489,12 @@ async function spawnGemini(command, options = {}, ws) {
|
||||
sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
|
||||
}
|
||||
|
||||
ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'gemini' }));
|
||||
// Terminal complete — skipped for aborted runs (abort-session
|
||||
// already sent the aborted complete on this run's behalf).
|
||||
if (!completeSent && !geminiProcess.aborted) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({ provider: 'gemini', sessionId: finalSessionId, exitCode: code }));
|
||||
}
|
||||
|
||||
// Clean up temporary image files if any
|
||||
if (geminiProcess.tempImagePaths && geminiProcess.tempImagePaths.length > 0) {
|
||||
@@ -566,6 +574,10 @@ async function spawnGemini(command, options = {}, ws) {
|
||||
|
||||
const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
||||
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' }));
|
||||
if (!completeSent && !geminiProcess.aborted) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({ provider: 'gemini', sessionId: errorSessionId, exitCode: 1 }));
|
||||
}
|
||||
notifyTerminalState({ error });
|
||||
|
||||
reject(error);
|
||||
@@ -590,6 +602,9 @@ function abortGeminiSession(sessionId) {
|
||||
|
||||
if (geminiProc) {
|
||||
try {
|
||||
// The abort handler sends the terminal complete (aborted: true);
|
||||
// flag the process so its close handler does not emit a second one.
|
||||
geminiProc.aborted = true;
|
||||
geminiProc.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (activeGeminiProcesses.has(processKey)) {
|
||||
|
||||
@@ -133,9 +133,10 @@ flowchart TD
|
||||
|
||||
### Chat Notes
|
||||
|
||||
1. `abort-session` returns a normalized `complete` message with `aborted: true`.
|
||||
2. `check-session-status` returns `{ type: "session-status", isProcessing }`.
|
||||
3. Claude status checks can reconnect output stream to the new socket via `reconnectSessionWriter`.
|
||||
1. **Unified terminal lifecycle**: every provider run ends with exactly one `complete` message built by `createCompleteMessage()` (`server/shared/utils.ts`), regardless of provider: `{ kind: "complete", sessionId, actualSessionId, exitCode, success, aborted }`. Failed runs emit an informational `error` message first, then the terminal `complete` with `success: false`. Mid-run `error` messages (e.g. stderr output) are non-terminal; the frontend only treats `complete` as end-of-run.
|
||||
2. `abort-session` sends the terminal `complete` (`aborted: true`) on behalf of the cancelled run; providers detect the abort and skip their own `complete` so the client sees exactly one.
|
||||
3. `check-session-status` returns `{ type: "session-status", isProcessing }`.
|
||||
4. Claude status checks can reconnect output stream to the new socket via `reconnectSessionWriter`.
|
||||
|
||||
## `/shell` Terminal Flow
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
AuthenticatedWebSocketRequest,
|
||||
LLMProvider,
|
||||
} from '@/shared/types.js';
|
||||
import { createNormalizedMessage, parseIncomingJsonObject } from '@/shared/utils.js';
|
||||
import { createCompleteMessage, parseIncomingJsonObject } from '@/shared/utils.js';
|
||||
|
||||
type ChatIncomingMessage = AnyRecord & {
|
||||
type?: string;
|
||||
@@ -173,14 +173,14 @@ export function handleChatConnection(
|
||||
success = await dependencies.abortClaudeSDKSession(sessionId);
|
||||
}
|
||||
|
||||
// Terminal complete on behalf of the cancelled run — providers skip
|
||||
// their own complete for aborted runs so the client sees exactly one.
|
||||
writer.send(
|
||||
createNormalizedMessage({
|
||||
kind: 'complete',
|
||||
createCompleteMessage({
|
||||
provider,
|
||||
sessionId,
|
||||
exitCode: success ? 0 : 1,
|
||||
aborted: true,
|
||||
success,
|
||||
sessionId,
|
||||
provider,
|
||||
})
|
||||
);
|
||||
return;
|
||||
@@ -202,13 +202,11 @@ export function handleChatConnection(
|
||||
const sessionId = typeof data.sessionId === 'string' ? data.sessionId : '';
|
||||
const success = dependencies.abortCursorSession(sessionId);
|
||||
writer.send(
|
||||
createNormalizedMessage({
|
||||
kind: 'complete',
|
||||
createCompleteMessage({
|
||||
provider: 'cursor',
|
||||
sessionId,
|
||||
exitCode: success ? 0 : 1,
|
||||
aborted: true,
|
||||
success,
|
||||
sessionId,
|
||||
provider: 'cursor',
|
||||
})
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { notifyRunFailed, notifyRunStopped } from './services/notification-orche
|
||||
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
||||
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
||||
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
||||
import { createNormalizedMessage } from './shared/utils.js';
|
||||
import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
|
||||
|
||||
// Track active sessions
|
||||
const activeCodexSessions = new Map();
|
||||
@@ -352,21 +352,26 @@ export async function queryCodex(command, options = {}, ws) {
|
||||
}
|
||||
}
|
||||
|
||||
// Send completion event
|
||||
if (!terminalFailure) {
|
||||
sendMessage(ws, createNormalizedMessage({
|
||||
kind: 'complete',
|
||||
actualSessionId: capturedSessionId || thread.id || sessionId || null,
|
||||
sessionId: capturedSessionId || sessionId || null,
|
||||
provider: 'codex'
|
||||
}));
|
||||
notifyRunStopped({
|
||||
userId: ws?.userId || null,
|
||||
// Send the terminal completion event — skipped for aborted runs, whose
|
||||
// terminal `complete` (aborted: true) was already sent by abort-session.
|
||||
const runSession = capturedSessionId ? activeCodexSessions.get(capturedSessionId) : null;
|
||||
const runAborted = runSession?.status === 'aborted' || abortController.signal.aborted;
|
||||
if (!runAborted) {
|
||||
sendMessage(ws, createCompleteMessage({
|
||||
provider: 'codex',
|
||||
sessionId: capturedSessionId || sessionId || null,
|
||||
sessionName: sessionSummary,
|
||||
stopReason: 'completed'
|
||||
});
|
||||
actualSessionId: capturedSessionId || thread.id || sessionId || null,
|
||||
exitCode: terminalFailure ? 1 : 0,
|
||||
}));
|
||||
if (!terminalFailure) {
|
||||
notifyRunStopped({
|
||||
userId: ws?.userId || null,
|
||||
provider: 'codex',
|
||||
sessionId: capturedSessionId || sessionId || null,
|
||||
sessionName: sessionSummary,
|
||||
stopReason: 'completed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -386,6 +391,11 @@ export async function queryCodex(command, options = {}, ws) {
|
||||
: error.message;
|
||||
|
||||
sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
|
||||
sendMessage(ws, createCompleteMessage({
|
||||
provider: 'codex',
|
||||
sessionId: capturedSessionId || sessionId || null,
|
||||
exitCode: 1,
|
||||
}));
|
||||
if (!terminalFailure) {
|
||||
notifyRunFailed({
|
||||
userId: ws?.userId || null,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { sessionsService } from './modules/providers/services/sessions.service.j
|
||||
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
||||
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
||||
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
||||
import { createNormalizedMessage, getOpenCodeDatabasePath } from './shared/utils.js';
|
||||
import { createCompleteMessage, createNormalizedMessage, getOpenCodeDatabasePath } from './shared/utils.js';
|
||||
|
||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
||||
|
||||
@@ -92,6 +92,9 @@ async function spawnOpenCode(command, options = {}, ws) {
|
||||
let stdoutLineBuffer = '';
|
||||
let terminalNotificationSent = false;
|
||||
let opencodeProcess = null;
|
||||
// Unified lifecycle contract: exactly one terminal `complete` per run
|
||||
// (close and error handlers can both fire for spawn failures).
|
||||
let completeSent = false;
|
||||
|
||||
const notifyTerminalState = ({ code = null, error = null } = {}) => {
|
||||
if (terminalNotificationSent) {
|
||||
@@ -256,13 +259,12 @@ async function spawnOpenCode(command, options = {}, ws) {
|
||||
}));
|
||||
}
|
||||
|
||||
ws.send(createNormalizedMessage({
|
||||
kind: 'complete',
|
||||
exitCode: code,
|
||||
isNewSession: !sessionId && !!command,
|
||||
sessionId: finalSessionId,
|
||||
provider: 'opencode',
|
||||
}));
|
||||
// Terminal complete — skipped for aborted runs (abort-session
|
||||
// already sent the aborted complete on this run's behalf).
|
||||
if (!completeSent && !opencodeProcess.aborted) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({ provider: 'opencode', sessionId: finalSessionId, exitCode: code }));
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
notifyTerminalState({ code });
|
||||
@@ -302,6 +304,10 @@ async function spawnOpenCode(command, options = {}, ws) {
|
||||
sessionId: finalSessionId,
|
||||
provider: 'opencode',
|
||||
}));
|
||||
if (!completeSent && !opencodeProcess.aborted) {
|
||||
completeSent = true;
|
||||
ws.send(createCompleteMessage({ provider: 'opencode', sessionId: finalSessionId, exitCode: 1 }));
|
||||
}
|
||||
notifyTerminalState({ error });
|
||||
reject(error);
|
||||
});
|
||||
@@ -315,6 +321,9 @@ function abortOpenCodeSession(sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The abort handler sends the terminal complete (aborted: true); flag the
|
||||
// process so its close handler does not emit a second one.
|
||||
process.aborted = true;
|
||||
process.kill('SIGTERM');
|
||||
activeOpenCodeProcesses.delete(sessionId);
|
||||
return true;
|
||||
|
||||
@@ -346,6 +346,43 @@ export function createNormalizedMessage(fields: NormalizedMessageInput): Normali
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the unified terminal `complete` lifecycle message.
|
||||
*
|
||||
* Contract: every provider run ends with exactly one `complete` (the
|
||||
* abort-session handler emits it on behalf of cancelled runs, so aborted runs
|
||||
* must NOT emit their own). The frontend treats `complete` as the only
|
||||
* terminal signal and never needs provider-specific handling:
|
||||
*
|
||||
* - `sessionId` — the id the client knows this run by ('' if never discovered)
|
||||
* - `actualSessionId` — canonical id after the run; equals `sessionId` unless
|
||||
* the provider rewrote it mid-run
|
||||
* - `exitCode` — 0 on success; a missing/null code (e.g. killed process)
|
||||
* is reported as failure
|
||||
* - `success` — exitCode === 0 and not aborted
|
||||
* - `aborted` — run was cancelled by the user
|
||||
*/
|
||||
export function createCompleteMessage(opts: {
|
||||
provider: NormalizedMessage['provider'];
|
||||
sessionId?: string | null;
|
||||
actualSessionId?: string | null;
|
||||
exitCode?: number | null;
|
||||
aborted?: boolean;
|
||||
}): NormalizedMessage {
|
||||
const exitCode = typeof opts.exitCode === 'number' ? opts.exitCode : 1;
|
||||
const aborted = Boolean(opts.aborted);
|
||||
|
||||
return createNormalizedMessage({
|
||||
kind: 'complete',
|
||||
provider: opts.provider,
|
||||
sessionId: opts.sessionId || null,
|
||||
actualSessionId: opts.actualSessionId || opts.sessionId || null,
|
||||
exitCode,
|
||||
success: exitCode === 0 && !aborted,
|
||||
aborted,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
//----------------- MCP CONFIG PARSING UTILITIES ------------
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user