feat: add chat unifier setup

This commit is contained in:
Haileyesus
2026-04-06 20:12:07 +03:00
parent bdab5a806f
commit 28a523b7a3
10 changed files with 4196 additions and 16 deletions

View File

@@ -93,6 +93,10 @@ export abstract class AbstractProvider implements IProvider {
timestamp: new Date().toISOString(),
channel: 'system',
message: 'Session stop requested.',
data: {
sessionId,
sessionStatus: 'SESSION_ABORTED',
},
});
}
@@ -220,6 +224,16 @@ export abstract class AbstractProvider implements IProvider {
thinkingMode: input.thinkingMode,
});
this.appendEvent(session, {
timestamp: session.startedAt,
channel: 'system',
message: 'Session started.',
data: {
sessionId,
sessionStatus: 'STARTED',
},
});
return session;
}

View File

@@ -240,6 +240,15 @@ export abstract class BaseCliProvider extends AbstractProvider {
if (code === 0) {
this.updateSessionStatus(session, 'completed');
this.appendEvent(session, {
timestamp: new Date().toISOString(),
channel: 'system',
message: 'Session completed.',
data: {
sessionId: session.sessionId,
sessionStatus: 'COMPLETED',
},
});
return;
}

View File

@@ -13,6 +13,7 @@ import type { LLMProvider } from '@/shared/types/app.js';
type CreateSdkExecutionInput = StartSessionInput & {
sessionId: string;
isResume: boolean;
emitEvent?: (event: ProviderSessionEvent) => void;
};
type SdkExecution = {
@@ -86,6 +87,9 @@ export abstract class BaseSdkProvider extends AbstractProvider {
...input,
model: effectiveModel,
thinkingMode: effectiveThinking,
emitEvent: (event) => {
this.appendEvent(session, event);
},
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to start SDK session';
@@ -123,6 +127,15 @@ export abstract class BaseSdkProvider extends AbstractProvider {
if (session.status === 'running') {
this.updateSessionStatus(session, 'completed');
this.appendEvent(session, {
timestamp: new Date().toISOString(),
channel: 'system',
message: 'Session completed.',
data: {
sessionId: session.sessionId,
sessionStatus: 'COMPLETED',
},
});
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown SDK execution failure';

View File

@@ -18,6 +18,7 @@ import type {
type ClaudeExecutionInput = StartSessionInput & {
sessionId: string;
isResume: boolean;
emitEvent?: (event: ProviderSessionEvent) => void;
};
const CLAUDE_THINKING_LEVELS = new Set(['low', 'medium', 'high', 'max']);
@@ -52,6 +53,18 @@ type ClaudeUserPromptMessage = {
timestamp: string;
};
/**
* Safely reads one optional string value from unknown data.
*/
const readString = (value: unknown): string | undefined => {
if (typeof value !== 'string') {
return undefined;
}
const normalized = value.trim();
return normalized.length ? normalized : undefined;
};
/**
* Claude SDK provider implementation.
*/
@@ -97,7 +110,7 @@ export class ClaudeProvider extends BaseSdkProvider {
cwd: input.workspacePath,
model: input.model,
effort: this.resolveClaudeEffort(input.thinkingMode),
canUseTool: this.resolvePermissionHandler(input.runtimePermissionMode),
canUseTool: this.resolvePermissionHandler(input.runtimePermissionMode, input.emitEvent),
};
if (input.isResume) {
@@ -232,20 +245,59 @@ export class ClaudeProvider extends BaseSdkProvider {
/**
* Builds a runtime permission callback when explicit allow/deny is requested.
*/
private resolvePermissionHandler(mode?: RuntimePermissionMode): CanUseTool | undefined {
private resolvePermissionHandler(
mode?: RuntimePermissionMode,
emitEvent?: (event: ProviderSessionEvent) => void,
): CanUseTool | undefined {
if (!mode || mode === 'ask') {
return undefined;
}
if (mode === 'allow') {
return async () => ({ behavior: 'allow' });
return async (toolName, input, options) => {
const optionsRecord = options as Record<string, unknown>;
emitEvent?.({
timestamp: new Date().toISOString(),
channel: 'system',
message: `Tool permission requested for "${toolName}".`,
data: {
type: 'tool_use_request',
toolName,
input,
toolUseID: options.toolUseID,
title: readString(optionsRecord.title),
displayName: readString(optionsRecord.displayName),
description: readString(optionsRecord.description),
blockedPath: options.blockedPath,
},
});
return { behavior: 'allow' };
};
}
return async () => ({
behavior: 'deny',
message: 'Permission denied by runtime permission mode.',
interrupt: false,
});
return async (toolName, input, options) => {
const optionsRecord = options as Record<string, unknown>;
emitEvent?.({
timestamp: new Date().toISOString(),
channel: 'system',
message: `Tool permission denied for "${toolName}".`,
data: {
type: 'tool_use_request',
toolName,
input,
toolUseID: options.toolUseID,
title: readString(optionsRecord.title),
displayName: readString(optionsRecord.displayName),
description: readString(optionsRecord.description),
blockedPath: options.blockedPath,
},
});
return {
behavior: 'deny',
message: 'Permission denied by runtime permission mode.',
interrupt: false,
};
};
}
/**