Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ad710e5da | ||
|
|
d2e5a26317 | ||
|
|
4f1eb4a8a9 | ||
|
|
e35bb708a2 | ||
|
|
cd2631428e | ||
|
|
09af399543 | ||
|
|
a9a648039f | ||
|
|
1d4ec7afb9 | ||
|
|
a1899951e0 | ||
|
|
d84668aa0f | ||
|
|
68d0f4574c | ||
|
|
bedf59bb48 | ||
|
|
793ea94078 |
@@ -104,6 +104,11 @@ interface AIChatSidePanelProps {
|
||||
username?: string;
|
||||
connected: boolean;
|
||||
}>;
|
||||
resolveExecutorContext?: (scope: {
|
||||
type: 'terminal' | 'workspace';
|
||||
targetId?: string;
|
||||
label?: string;
|
||||
}) => ExecutorContext;
|
||||
|
||||
// Visibility
|
||||
isVisible?: boolean;
|
||||
@@ -179,6 +184,7 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
scopeHostIds,
|
||||
scopeLabel,
|
||||
terminalSessions = [],
|
||||
resolveExecutorContext,
|
||||
isVisible = true,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
@@ -200,12 +206,8 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
const { openSettingsWindow } = useWindowControls();
|
||||
const terminalSessionsRef = useRef(terminalSessions);
|
||||
terminalSessionsRef.current = terminalSessions;
|
||||
const scopeTypeRef = useRef(scopeType);
|
||||
scopeTypeRef.current = scopeType;
|
||||
const scopeTargetIdRef = useRef(scopeTargetId);
|
||||
scopeTargetIdRef.current = scopeTargetId;
|
||||
const scopeLabelRef = useRef(scopeLabel);
|
||||
scopeLabelRef.current = scopeLabel;
|
||||
const resolveExecutorContextRef = useRef(resolveExecutorContext);
|
||||
resolveExecutorContextRef.current = resolveExecutorContext;
|
||||
|
||||
// ── Streaming hook ──
|
||||
const {
|
||||
@@ -416,11 +418,19 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
}
|
||||
}, [updateSessionTitle]);
|
||||
|
||||
const getExecutorContext = useCallback((): ExecutorContext => ({
|
||||
sessions: terminalSessionsRef.current,
|
||||
workspaceId: scopeTypeRef.current === 'workspace' ? scopeTargetIdRef.current : undefined,
|
||||
workspaceName: scopeTypeRef.current === 'workspace' ? scopeLabelRef.current : undefined,
|
||||
}), []);
|
||||
const buildExecutorContextForScope = useCallback((scope: {
|
||||
type: 'terminal' | 'workspace';
|
||||
targetId?: string;
|
||||
label?: string;
|
||||
}): ExecutorContext => {
|
||||
const resolved = resolveExecutorContextRef.current?.(scope);
|
||||
if (resolved) return resolved;
|
||||
return {
|
||||
sessions: terminalSessionsRef.current,
|
||||
workspaceId: scope.type === 'workspace' ? scope.targetId : undefined,
|
||||
workspaceName: scope.type === 'workspace' ? scope.label : undefined,
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** Ensure a session exists for the current scope and return its ID. */
|
||||
const ensureSession = useCallback((): string => {
|
||||
@@ -504,6 +514,11 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
abortControllersRef.current.delete(sessionId);
|
||||
autoTitleSession(sessionId, trimmed);
|
||||
} else {
|
||||
const toolScope = {
|
||||
type: scopeType,
|
||||
targetId: scopeTargetId,
|
||||
label: scopeLabel,
|
||||
} as const;
|
||||
await sendToCattyAgent(sessionId, sendScopeKey, trimmed, abortController, currentSession ?? undefined, assistantMsgId, {
|
||||
activeProvider,
|
||||
activeModelId,
|
||||
@@ -514,7 +529,7 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
commandBlocklist,
|
||||
terminalSessions,
|
||||
webSearchConfig,
|
||||
getExecutorContext,
|
||||
getExecutorContext: () => buildExecutorContextForScope(toolScope),
|
||||
setPendingApproval,
|
||||
autoTitleSession,
|
||||
});
|
||||
@@ -526,7 +541,7 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
setStreamingForScope, setInputValue, clearImages,
|
||||
sendToExternalAgent, sendToCattyAgent, reportStreamError, autoTitleSession, t,
|
||||
abortControllersRef, terminalSessions, providers, selectedAgentModel, updateSessionExternalSessionId,
|
||||
scopeType, scopeTargetId, scopeLabel, globalPermissionMode, commandBlocklist, webSearchConfig, getExecutorContext, setPendingApproval,
|
||||
scopeType, scopeTargetId, scopeLabel, globalPermissionMode, commandBlocklist, webSearchConfig, buildExecutorContextForScope, setPendingApproval,
|
||||
]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
@@ -639,19 +654,11 @@ const AIChatSidePanelInner: React.FC<AIChatSidePanelProps> = ({
|
||||
messages={messages}
|
||||
isStreaming={isStreaming}
|
||||
onApprove={(messageId) => void handleApprovalResponse(messageId, true, {
|
||||
terminalSessions,
|
||||
scopeType,
|
||||
scopeTargetId,
|
||||
scopeLabel,
|
||||
globalPermissionMode,
|
||||
commandBlocklist,
|
||||
webSearchConfig,
|
||||
})}
|
||||
onReject={(messageId) => void handleApprovalResponse(messageId, false, {
|
||||
terminalSessions,
|
||||
scopeType,
|
||||
scopeTargetId,
|
||||
scopeLabel,
|
||||
globalPermissionMode,
|
||||
commandBlocklist,
|
||||
webSearchConfig,
|
||||
|
||||
@@ -61,16 +61,6 @@ interface TreeNodeProps {
|
||||
toggleHostSelection?: (hostId: string) => void;
|
||||
}
|
||||
|
||||
// Helper function to recursively count all hosts in a node and its children
|
||||
const countAllHostsInNode = (node: GroupNode): number => {
|
||||
let count = node.hosts.length;
|
||||
if (node.children) {
|
||||
Object.values(node.children).forEach((child) => {
|
||||
count += countAllHostsInNode(child);
|
||||
});
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
node,
|
||||
@@ -100,7 +90,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
const hasChildren = node.children && Object.keys(node.children).length > 0;
|
||||
const paddingLeft = `${depth * 20 + 12}px`;
|
||||
const isManaged = managedGroupPaths?.has(node.path) ?? false;
|
||||
const hostsCountInNode = useMemo(() => countAllHostsInNode(node), [node]);
|
||||
const hostsCountInNode = node.totalHostCount ?? node.hosts.length;
|
||||
|
||||
const childNodes = useMemo(() => {
|
||||
if (!node.children) return [];
|
||||
|
||||
@@ -621,7 +621,7 @@ echo $3 >> "$FILE"`);
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
</div>
|
||||
<DropdownContent className="w-44" align="start" alignToParent>
|
||||
<DropdownContent className="w-48" align="start" alignToParent>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start gap-2"
|
||||
|
||||
@@ -244,6 +244,12 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
activeTabIdRef.current = activeTabId;
|
||||
const activeWorkspaceRef = useRef(activeWorkspace);
|
||||
activeWorkspaceRef.current = activeWorkspace;
|
||||
const sessionsRef = useRef(sessions);
|
||||
sessionsRef.current = sessions;
|
||||
const workspacesRef = useRef(workspaces);
|
||||
workspacesRef.current = workspaces;
|
||||
const hostsRef = useRef(hosts);
|
||||
hostsRef.current = hosts;
|
||||
const onSetWorkspaceFocusedSessionRef = useRef(onSetWorkspaceFocusedSession);
|
||||
onSetWorkspaceFocusedSessionRef.current = onSetWorkspaceFocusedSession;
|
||||
|
||||
@@ -965,6 +971,44 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
return result;
|
||||
}, [sessions, hosts, activeWorkspace, activeSession]);
|
||||
|
||||
const resolveAIExecutorContext = useCallback((scope: {
|
||||
type: 'terminal' | 'workspace';
|
||||
targetId?: string;
|
||||
label?: string;
|
||||
}) => {
|
||||
const latestWorkspaces = workspacesRef.current;
|
||||
const latestSessions = sessionsRef.current;
|
||||
const latestHosts = hostsRef.current;
|
||||
const sessionIds = scope.type === 'workspace'
|
||||
? (() => {
|
||||
const workspace = scope.targetId ? latestWorkspaces.find((w) => w.id === scope.targetId) : undefined;
|
||||
return workspace?.root ? collectSessionIds(workspace.root) : [];
|
||||
})()
|
||||
: scope.targetId ? [scope.targetId] : [];
|
||||
|
||||
const workspaceName = scope.type === 'workspace'
|
||||
? latestWorkspaces.find((w) => w.id === scope.targetId)?.title ?? scope.label
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
sessions: sessionIds.map((sid) => {
|
||||
const session = latestSessions.find((s) => s.id === sid);
|
||||
const host = session?.hostId ? latestHosts.find((h) => h.id === session.hostId) : undefined;
|
||||
return {
|
||||
sessionId: sid,
|
||||
hostId: session?.hostId || '',
|
||||
hostname: host?.hostname || '',
|
||||
label: host?.label || session?.hostLabel || '',
|
||||
os: host?.os,
|
||||
username: host?.username,
|
||||
connected: session?.status === 'connected',
|
||||
};
|
||||
}),
|
||||
workspaceId: scope.type === 'workspace' ? scope.targetId : undefined,
|
||||
workspaceName,
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Subscribe to custom theme changes so editing triggers re-render
|
||||
const customThemes = useCustomThemes();
|
||||
|
||||
@@ -1360,6 +1404,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
}
|
||||
scopeLabel={activeWorkspace?.title ?? activeSession?.hostLabel ?? ''}
|
||||
terminalSessions={aiTerminalSessions}
|
||||
resolveExecutorContext={resolveAIExecutorContext}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -689,6 +689,15 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
],
|
||||
);
|
||||
|
||||
const countAllHostsInNode = (node: GroupNode): number => {
|
||||
let count = node.hosts.length;
|
||||
Object.values(node.children).forEach((child) => {
|
||||
count += countAllHostsInNode(child);
|
||||
});
|
||||
node.totalHostCount = count;
|
||||
return count;
|
||||
};
|
||||
|
||||
const buildGroupTree = useMemo<Record<string, GroupNode>>(() => {
|
||||
const root: Record<string, GroupNode> = {};
|
||||
const insertPath = (path: string, host?: Host) => {
|
||||
@@ -712,6 +721,9 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
};
|
||||
customGroups.forEach((path) => insertPath(path));
|
||||
hosts.forEach((host) => insertPath(host.group || "General", host));
|
||||
|
||||
Object.values(root).forEach(countAllHostsInNode);
|
||||
|
||||
return root;
|
||||
}, [hosts, customGroups]);
|
||||
|
||||
@@ -896,20 +908,12 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
insertPath(host.group, host);
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(root).forEach(countAllHostsInNode);
|
||||
|
||||
return root;
|
||||
}, [treeViewHosts, customGroups]);
|
||||
|
||||
// Helper function to recursively count all hosts in a node and its children
|
||||
const countAllHostsInNode = (node: GroupNode): number => {
|
||||
let count = node.hosts.length;
|
||||
if (node.children) {
|
||||
Object.values(node.children).forEach((child) => {
|
||||
count += countAllHostsInNode(child);
|
||||
});
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
// Create tree view specific group tree that excludes ungrouped hosts
|
||||
const treeViewGroupTree = useMemo<GroupNode[]>(() => {
|
||||
return (Object.values(buildTreeViewGroupTree) as GroupNode[]).sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -1749,7 +1753,7 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{t("vault.groups.hostsCount", { count: countAllHostsInNode(node) })}
|
||||
{t("vault.groups.hostsCount", { count: node.totalHostCount ?? node.hosts.length })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,9 @@ const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, isStreaming
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20 text-sm">
|
||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-destructive font-medium">{message.errorInfo.message}</p>
|
||||
<p className="text-destructive font-medium whitespace-pre-wrap break-words [overflow-wrap:anywhere]">
|
||||
{message.errorInfo.message}
|
||||
</p>
|
||||
{message.errorInfo.retryable && (
|
||||
<p className="text-muted-foreground text-xs mt-1">{t('ai.chat.retryHint')}</p>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { createCattyTools } from '../../../infrastructure/ai/sdk/tools';
|
||||
import type { NetcattyBridge, ExecutorContext } from '../../../infrastructure/ai/cattyAgent/executor';
|
||||
import { runExternalAgentTurn } from '../../../infrastructure/ai/externalAgentAdapter';
|
||||
import { runAcpAgentTurn } from '../../../infrastructure/ai/acpAgentAdapter';
|
||||
import { classifyError, sanitizeErrorMessage } from '../../../infrastructure/ai/errorClassifier';
|
||||
import { classifyError } from '../../../infrastructure/ai/errorClassifier';
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Stream chunk type interfaces (Issue #13: replace unsafe casts)
|
||||
@@ -135,6 +135,9 @@ export interface PendingApprovalContext {
|
||||
model: ReturnType<typeof createModelFromConfig>;
|
||||
systemPrompt: string;
|
||||
tools: ReturnType<typeof createCattyTools>;
|
||||
scopeType: 'terminal' | 'workspace';
|
||||
scopeLabel?: string;
|
||||
getExecutorContext: () => ExecutorContext;
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
@@ -294,8 +297,6 @@ export function useAIChatStreaming({
|
||||
// Log the full unsanitized error for debugging
|
||||
console.error('[AIChatSidePanel] Stream error (full):', errorStr);
|
||||
const errorInfo = classifyError(errorStr);
|
||||
// Sanitize the displayed message to avoid leaking paths, keys, or other sensitive info
|
||||
errorInfo.message = sanitizeErrorMessage(errorInfo.message);
|
||||
updateLastMessage(sessionId, msg => ({
|
||||
...msg,
|
||||
statusText: '',
|
||||
@@ -669,14 +670,14 @@ export function useAIChatStreaming({
|
||||
context: SendToCattyContext,
|
||||
) => {
|
||||
const bridge = getNetcattyBridge();
|
||||
const toolContext = context.getExecutorContext ?? (() => ({
|
||||
const getExecutorContext = context.getExecutorContext ?? (() => ({
|
||||
sessions: context.terminalSessions,
|
||||
workspaceId: context.scopeType === 'workspace' ? context.scopeTargetId : undefined,
|
||||
workspaceName: context.scopeType === 'workspace' ? context.scopeLabel : undefined,
|
||||
}));
|
||||
const tools = createCattyTools(
|
||||
bridge,
|
||||
toolContext,
|
||||
getExecutorContext,
|
||||
context.commandBlocklist,
|
||||
context.globalPermissionMode,
|
||||
context.webSearchConfig ?? undefined,
|
||||
@@ -785,7 +786,16 @@ export function useAIChatStreaming({
|
||||
|
||||
if (approvalInfo) {
|
||||
context.setPendingApproval({
|
||||
sessionId, scopeKey: sendScopeKey, sdkMessages, approvalInfo, model, systemPrompt, tools,
|
||||
sessionId,
|
||||
scopeKey: sendScopeKey,
|
||||
sdkMessages,
|
||||
approvalInfo,
|
||||
model,
|
||||
systemPrompt,
|
||||
tools,
|
||||
scopeType: context.scopeType,
|
||||
scopeLabel: context.scopeLabel,
|
||||
getExecutorContext,
|
||||
});
|
||||
return; // Keep streaming flag — waiting for user approval
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { classifyError } from '../../../infrastructure/ai/errorClassifier';
|
||||
import type {
|
||||
ApprovalInfo,
|
||||
PendingApprovalContext,
|
||||
TerminalSessionInfo,
|
||||
} from './useAIChatStreaming';
|
||||
import { getNetcattyBridge } from './useAIChatStreaming';
|
||||
import type { createModelFromConfig } from '../../../infrastructure/ai/sdk/providers';
|
||||
@@ -75,10 +74,6 @@ export interface UseToolApprovalReturn {
|
||||
|
||||
/** Context values needed by handleApprovalResponse that change frequently. */
|
||||
export interface ToolApprovalContext {
|
||||
terminalSessions: TerminalSessionInfo[];
|
||||
scopeType: 'terminal' | 'workspace';
|
||||
scopeTargetId?: string;
|
||||
scopeLabel?: string;
|
||||
globalPermissionMode: AIPermissionMode;
|
||||
commandBlocklist?: string[];
|
||||
webSearchConfig?: WebSearchConfig | null;
|
||||
@@ -146,7 +141,16 @@ export function useToolApproval({
|
||||
const ctx = pendingApprovalContextRef.current;
|
||||
if (!ctx) return;
|
||||
// Destructure all needed values BEFORE clearing the ref to avoid race conditions
|
||||
const { sessionId: sid, scopeKey: sk, sdkMessages, approvalInfo, model: ctxModel } = ctx;
|
||||
const {
|
||||
sessionId: sid,
|
||||
scopeKey: sk,
|
||||
sdkMessages,
|
||||
approvalInfo,
|
||||
model: ctxModel,
|
||||
scopeType,
|
||||
scopeLabel,
|
||||
getExecutorContext,
|
||||
} = ctx;
|
||||
// Clear pending approval (and its timeout) via setPendingApproval
|
||||
setPendingApproval(null);
|
||||
|
||||
@@ -218,16 +222,20 @@ export function useToolApproval({
|
||||
|
||||
try {
|
||||
// Rebuild tools and system prompt with the latest permission mode to prevent
|
||||
// stale closure issues (e.g. user changed permission mode during approval wait)
|
||||
// stale settings, while keeping the original AI scope pinned to its workspace/session.
|
||||
const bridge = getNetcattyBridge();
|
||||
const freshTools = createCattyTools(bridge, {
|
||||
sessions: approvalContext.terminalSessions,
|
||||
workspaceId: approvalContext.scopeType === 'workspace' ? approvalContext.scopeTargetId : undefined,
|
||||
workspaceName: approvalContext.scopeType === 'workspace' ? approvalContext.scopeLabel : undefined,
|
||||
}, approvalContext.commandBlocklist, approvalContext.globalPermissionMode, approvalContext.webSearchConfig ?? undefined);
|
||||
const freshExecutorContext = getExecutorContext();
|
||||
const freshTools = createCattyTools(
|
||||
bridge,
|
||||
getExecutorContext,
|
||||
approvalContext.commandBlocklist,
|
||||
approvalContext.globalPermissionMode,
|
||||
approvalContext.webSearchConfig ?? undefined,
|
||||
);
|
||||
const freshSystemPrompt = buildSystemPrompt({
|
||||
scopeType: approvalContext.scopeType, scopeLabel: approvalContext.scopeLabel,
|
||||
hosts: approvalContext.terminalSessions.map(s => ({
|
||||
scopeType,
|
||||
scopeLabel,
|
||||
hosts: freshExecutorContext.sessions.map(s => ({
|
||||
sessionId: s.sessionId, hostname: s.hostname, label: s.label,
|
||||
os: s.os, username: s.username, connected: s.connected,
|
||||
})),
|
||||
@@ -246,6 +254,9 @@ export function useToolApproval({
|
||||
model: ctxModel,
|
||||
systemPrompt: freshSystemPrompt,
|
||||
tools: freshTools,
|
||||
scopeType,
|
||||
scopeLabel,
|
||||
getExecutorContext,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,8 @@ export interface GroupNode {
|
||||
path: string;
|
||||
children: Record<string, GroupNode>;
|
||||
hosts: Host[];
|
||||
/** Pre-computed total host count including all descendants. Set during tree construction. */
|
||||
totalHostCount?: number;
|
||||
}
|
||||
|
||||
export interface SyncConfig {
|
||||
|
||||
@@ -1,47 +1,15 @@
|
||||
import type { ChatMessage } from './types';
|
||||
|
||||
/**
|
||||
* Classifies a raw error string into structured error info for display.
|
||||
* Convert a raw error string into display-safe error info.
|
||||
*
|
||||
* Intentionally avoids keyword-based "root cause" attribution because upstream
|
||||
* providers often return generic 4xx/5xx text that would be misclassified.
|
||||
* We show the sanitized upstream message directly instead.
|
||||
*/
|
||||
export function classifyError(error: string): NonNullable<ChatMessage['errorInfo']> {
|
||||
const lower = error.toLowerCase();
|
||||
|
||||
// Network errors
|
||||
if (lower.includes('econnrefused') || lower.includes('enotfound') || lower.includes('enetunreach') || lower.includes('fetch failed') || lower.includes('network')) {
|
||||
return { type: 'network', message: 'Network connection failed. Please check your internet connection and API endpoint.', retryable: true };
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if (lower.includes('timeout') || lower.includes('timed out') || lower.includes('econnreset') || lower.includes('socket hang up')) {
|
||||
return { type: 'timeout', message: 'Request timed out. The server may be overloaded or unreachable.', retryable: true };
|
||||
}
|
||||
|
||||
// Auth errors
|
||||
if (lower.includes('401') || lower.includes('403') || lower.includes('unauthorized') || lower.includes('invalid api key') || lower.includes('authentication')) {
|
||||
return { type: 'auth', message: 'Authentication failed. Please check your API key in Settings \u2192 AI.', retryable: false };
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
if (lower.includes('429') || lower.includes('rate limit') || lower.includes('too many requests')) {
|
||||
return { type: 'provider', message: 'Rate limit exceeded. Please wait a moment before retrying.', retryable: true };
|
||||
}
|
||||
|
||||
// Provider errors (5xx)
|
||||
if (/\b5\d{2}\b/.test(error) || lower.includes('server error') || lower.includes('internal error')) {
|
||||
return { type: 'provider', message: sanitizeErrorMessage(error), retryable: true };
|
||||
}
|
||||
|
||||
// Model not found
|
||||
if (lower.includes('model not found') || lower.includes('does not exist') || lower.includes('404')) {
|
||||
return { type: 'provider', message: 'Model not found. Please check your model selection in Settings \u2192 AI.', retryable: false };
|
||||
}
|
||||
|
||||
// Command blocked
|
||||
if (lower.includes('blocked by safety')) {
|
||||
return { type: 'agent', message: sanitizeErrorMessage(error), retryable: false };
|
||||
}
|
||||
|
||||
return { type: 'unknown', message: sanitizeErrorMessage(error), retryable: true };
|
||||
const message = sanitizeErrorMessage(error).trim() || 'Unknown error';
|
||||
return { type: 'unknown', message, retryable: false };
|
||||
}
|
||||
|
||||
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
||||
|
||||
Reference in New Issue
Block a user