Initial public release
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
|
||||
export interface DebugMessageInfo {
|
||||
messageId: string;
|
||||
role: string;
|
||||
timestamp: number;
|
||||
partsCount: number;
|
||||
parts: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
text?: string;
|
||||
textLength?: number;
|
||||
tool?: string;
|
||||
state?: any;
|
||||
}>;
|
||||
isEmpty: boolean;
|
||||
isEmptyResponse: boolean;
|
||||
raw: any;
|
||||
}
|
||||
|
||||
export const debugUtils = {
|
||||
|
||||
getLastAssistantMessage(): DebugMessageInfo | null {
|
||||
const state = useSessionStore.getState();
|
||||
const currentSessionId = state.currentSessionId;
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('[ERROR] No active session');
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages = state.messages.get(currentSessionId);
|
||||
if (!messages || messages.length === 0) {
|
||||
console.log('[ERROR] No messages in current session');
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.info.role === 'assistant') {
|
||||
const parts = msg.parts.map((part: any) => {
|
||||
const info: any = {
|
||||
id: part.id,
|
||||
type: part.type,
|
||||
};
|
||||
|
||||
if (part.type === 'text') {
|
||||
info.text = part.text;
|
||||
info.textLength = part.text?.length || 0;
|
||||
} else if (part.type === 'tool') {
|
||||
info.tool = part.tool;
|
||||
info.state = part.state?.status;
|
||||
} else if (part.type === 'step-start' || part.type === 'step-finish') {
|
||||
info.isStepMarker = true;
|
||||
}
|
||||
|
||||
return info;
|
||||
});
|
||||
|
||||
const hasText = parts.some((p: any) => p.type === 'text' && p.text && p.text.trim().length > 0);
|
||||
const hasTools = parts.some((p: any) => p.type === 'tool');
|
||||
const hasStepMarkers = parts.some((p: any) => p.type === 'step-start' || p.type === 'step-finish');
|
||||
const isEmpty = parts.length === 0;
|
||||
const isEmptyResponse = !hasText && !hasTools && (!isEmpty || hasStepMarkers);
|
||||
|
||||
const info: DebugMessageInfo = {
|
||||
messageId: msg.info.id,
|
||||
role: msg.info.role,
|
||||
timestamp: msg.info.time?.created || 0,
|
||||
partsCount: parts.length,
|
||||
parts,
|
||||
isEmpty,
|
||||
isEmptyResponse,
|
||||
raw: msg,
|
||||
};
|
||||
|
||||
console.log('[INSPECT] Last Assistant Message:', info);
|
||||
console.log('[SUMMARY] Summary:', {
|
||||
messageId: info.messageId,
|
||||
partsCount: info.partsCount,
|
||||
isEmpty: info.isEmpty,
|
||||
isEmptyResponse: info.isEmptyResponse,
|
||||
hasText,
|
||||
hasTools,
|
||||
hasStepMarkers,
|
||||
onlyStepMarkers: hasStepMarkers && !hasText && !hasTools,
|
||||
});
|
||||
|
||||
if (info.isEmpty) {
|
||||
console.warn('[WARNING] Message has NO parts!');
|
||||
}
|
||||
|
||||
if (info.isEmptyResponse) {
|
||||
console.warn('[WARNING] Message has parts but NO meaningful content (empty text, no tools)!');
|
||||
|
||||
if (hasStepMarkers && !hasText && !hasTools) {
|
||||
console.warn('[CRITICAL] CLAUDE EMPTY RESPONSE BUG: Only step-start/step-finish markers, no actual content!');
|
||||
console.log('This is a known issue with Claude models (anthropic provider)');
|
||||
console.log('Recommendation: Send a follow-up message or try a different model');
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ERROR] No assistant messages found in current session');
|
||||
return null;
|
||||
},
|
||||
|
||||
truncateString(value: string | undefined, maxLength: number = 80): string | undefined {
|
||||
if (!value || typeof value !== 'string') return value;
|
||||
if (value.length <= maxLength) return value;
|
||||
return value.substring(0, maxLength) + '…';
|
||||
},
|
||||
|
||||
truncateMessages(messages: any[]): any[] {
|
||||
return messages.map((msg) => ({
|
||||
...msg,
|
||||
parts: (msg.parts || []).map((part: any) => {
|
||||
const truncatedPart: any = { ...part };
|
||||
|
||||
if ('text' in part) {
|
||||
truncatedPart.text = this.truncateString(part.text);
|
||||
}
|
||||
if ('textPreview' in part) {
|
||||
truncatedPart.textPreview = this.truncateString(part.textPreview);
|
||||
}
|
||||
|
||||
if (part.state) {
|
||||
truncatedPart.state = { ...part.state };
|
||||
|
||||
if ('output' in part.state) {
|
||||
truncatedPart.state.output = this.truncateString(part.state.output);
|
||||
}
|
||||
if ('error' in part.state) {
|
||||
truncatedPart.state.error = this.truncateString(part.state.error);
|
||||
}
|
||||
if (part.state.metadata && 'preview' in part.state.metadata) {
|
||||
truncatedPart.state.metadata = {
|
||||
...part.state.metadata,
|
||||
preview: this.truncateString(part.state.metadata.preview),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return truncatedPart;
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
getAllMessages(truncate: boolean = false) {
|
||||
const state = useSessionStore.getState();
|
||||
const currentSessionId = state.currentSessionId;
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('[ERROR] No active session');
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages = state.messages.get(currentSessionId) || [];
|
||||
console.log(`[MESSAGES] Total messages in session: ${messages.length}`);
|
||||
|
||||
messages.forEach((msg, idx) => {
|
||||
console.log(`[${idx}] ${msg.info.role} - ${msg.info.id} - ${msg.parts.length} parts`);
|
||||
});
|
||||
|
||||
return truncate ? this.truncateMessages(messages) : messages;
|
||||
},
|
||||
|
||||
checkLastMessage() {
|
||||
const info = this.getLastAssistantMessage();
|
||||
if (!info) return false;
|
||||
|
||||
const isProblematic = info.isEmpty || info.isEmptyResponse;
|
||||
|
||||
if (isProblematic) {
|
||||
console.error('[ALERT] PROBLEMATIC MESSAGE DETECTED!');
|
||||
console.log('Details:', {
|
||||
messageId: info.messageId,
|
||||
isEmpty: info.isEmpty,
|
||||
isEmptyResponse: info.isEmptyResponse,
|
||||
partsCount: info.partsCount,
|
||||
});
|
||||
|
||||
if (info.parts.length > 0) {
|
||||
console.log('Parts:', info.parts);
|
||||
}
|
||||
} else {
|
||||
console.log('[OK] Last message looks good!');
|
||||
}
|
||||
|
||||
return isProblematic;
|
||||
},
|
||||
|
||||
getStreamingState() {
|
||||
const state = useSessionStore.getState();
|
||||
const currentStreamingId = state.currentSessionId
|
||||
? state.streamingMessageIds.get(state.currentSessionId) ?? null
|
||||
: null;
|
||||
console.log('[STREAM] Streaming State:', {
|
||||
streamingMessageId: currentStreamingId,
|
||||
streamingMessageIds: Array.from(state.streamingMessageIds.entries()),
|
||||
messageStreamStates: Array.from(state.messageStreamStates.entries()),
|
||||
});
|
||||
return {
|
||||
streamingMessageId: currentStreamingId,
|
||||
streamingMessageIds: state.streamingMessageIds,
|
||||
streamStates: state.messageStreamStates,
|
||||
};
|
||||
},
|
||||
|
||||
findEmptyMessages() {
|
||||
const state = useSessionStore.getState();
|
||||
const currentSessionId = state.currentSessionId;
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('[ERROR] No active session');
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages = state.messages.get(currentSessionId) || [];
|
||||
const emptyMessages = messages
|
||||
.filter((msg) => msg.info.role === 'assistant')
|
||||
.filter((msg) => {
|
||||
const parts = msg.parts || [];
|
||||
const hasTextContent = parts.some(
|
||||
(p: any) => p.type === 'text' && p.text && p.text.trim().length > 0
|
||||
);
|
||||
const hasTools = parts.some((p: any) => p.type === 'tool');
|
||||
|
||||
return parts.length === 0 || (!hasTextContent && !hasTools);
|
||||
});
|
||||
|
||||
console.log(`[INSPECT] Found ${emptyMessages.length} empty assistant messages`);
|
||||
|
||||
emptyMessages.forEach((msg, idx) => {
|
||||
console.log(`[${idx}] Empty message:`, {
|
||||
messageId: msg.info.id,
|
||||
partsCount: msg.parts.length,
|
||||
provider: (msg.info as any).providerID,
|
||||
model: (msg.info as any).modelID,
|
||||
timestamp: msg.info.time?.created,
|
||||
});
|
||||
});
|
||||
|
||||
return emptyMessages;
|
||||
},
|
||||
|
||||
showRetryHelp() {
|
||||
console.log('[DEBUG] How to handle empty Claude responses:\n');
|
||||
console.log('1. Check the last message:');
|
||||
console.log(' __opencodeDebug.getLastAssistantMessage()\n');
|
||||
console.log('2. Find all empty messages in session:');
|
||||
console.log(' __opencodeDebug.findEmptyMessages()\n');
|
||||
console.log('3. To retry, you can:');
|
||||
console.log(' - Edit your last user message and resend');
|
||||
console.log(' - Send a follow-up message like "Please provide the response"');
|
||||
console.log(' - Try a different model (OpenAI models tend to be more reliable)\n');
|
||||
console.log('[TIP] Empty responses are usually due to:');
|
||||
console.log(' - Model rate limits');
|
||||
console.log(' - Context length issues');
|
||||
console.log(' - Model refusing to respond to certain prompts');
|
||||
console.log(' - API errors from provider');
|
||||
},
|
||||
|
||||
analyzeMessageCompletionConsistency(options: {
|
||||
includeNonAssistant?: boolean;
|
||||
verbose?: boolean;
|
||||
maxTableRows?: number;
|
||||
} = {}) {
|
||||
const { includeNonAssistant = false, verbose = true, maxTableRows = 25 } = options;
|
||||
const state = useSessionStore.getState();
|
||||
const currentSessionId = state.currentSessionId;
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('[ERROR] No active session');
|
||||
return { summary: null, rows: [] };
|
||||
}
|
||||
|
||||
const messages = state.messages.get(currentSessionId) || [];
|
||||
const targetMessages = includeNonAssistant
|
||||
? messages
|
||||
: messages.filter((msg) => msg.info.role === 'assistant');
|
||||
|
||||
const summary = {
|
||||
totalMessages: messages.length,
|
||||
analyzedMessages: targetMessages.length,
|
||||
completedMissing: 0,
|
||||
completedBeforeTool: 0,
|
||||
completedBeforeReasoning: 0,
|
||||
runningToolsWhenCompleted: 0,
|
||||
reasoningOpenWhenCompleted: 0,
|
||||
withTools: 0,
|
||||
withReasoning: 0,
|
||||
};
|
||||
|
||||
const toNumber = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
|
||||
const getLatestTimestamp = (timestamps: Array<number | null>): number | null => {
|
||||
const filtered = timestamps.filter((value): value is number => typeof value === 'number');
|
||||
return filtered.length > 0 ? Math.max(...filtered) : null;
|
||||
};
|
||||
|
||||
const rows = targetMessages.map((message, index) => {
|
||||
const info = message.info ?? {};
|
||||
const parts = Array.isArray(message.parts) ? message.parts : [];
|
||||
|
||||
const timeInfo = (info.time ?? {}) as { completed?: number };
|
||||
const completedAt = toNumber(timeInfo.completed);
|
||||
const hasCompleted = completedAt !== null;
|
||||
|
||||
const toolParts = parts.filter((part: any) => part.type === 'tool');
|
||||
const reasoningParts = parts.filter((part: any) => part.type === 'reasoning');
|
||||
|
||||
const latestToolTimestamp = getLatestTimestamp(
|
||||
toolParts.map((part: any) =>
|
||||
toNumber(part.state?.time?.end ?? part.state?.time?.start)
|
||||
)
|
||||
);
|
||||
const latestReasoningTimestamp = getLatestTimestamp(
|
||||
reasoningParts.map((part: any) => toNumber(part.time?.end ?? part.time?.start))
|
||||
);
|
||||
const latestPartTimestamp = getLatestTimestamp(
|
||||
[latestToolTimestamp, latestReasoningTimestamp].filter((value) => value !== null)
|
||||
);
|
||||
|
||||
const hasRunningTool = toolParts.some((part: any) =>
|
||||
['pending', 'running', 'started'].includes(part.state?.status)
|
||||
);
|
||||
const reasoningIncomplete = reasoningParts.some(
|
||||
(part: any) => typeof part.time?.end !== 'number'
|
||||
);
|
||||
|
||||
if (toolParts.length > 0) {
|
||||
summary.withTools += 1;
|
||||
}
|
||||
if (reasoningParts.length > 0) {
|
||||
summary.withReasoning += 1;
|
||||
}
|
||||
|
||||
if (!hasCompleted) {
|
||||
summary.completedMissing += 1;
|
||||
}
|
||||
|
||||
const completedBeforeTool = Boolean(
|
||||
hasCompleted &&
|
||||
typeof latestToolTimestamp === 'number' &&
|
||||
completedAt! < latestToolTimestamp
|
||||
);
|
||||
if (completedBeforeTool) {
|
||||
summary.completedBeforeTool += 1;
|
||||
}
|
||||
|
||||
const completedBeforeReasoning = Boolean(
|
||||
hasCompleted &&
|
||||
typeof latestReasoningTimestamp === 'number' &&
|
||||
completedAt! < latestReasoningTimestamp
|
||||
);
|
||||
if (completedBeforeReasoning) {
|
||||
summary.completedBeforeReasoning += 1;
|
||||
}
|
||||
|
||||
if (hasCompleted && hasRunningTool) {
|
||||
summary.runningToolsWhenCompleted += 1;
|
||||
}
|
||||
if (hasCompleted && reasoningIncomplete) {
|
||||
summary.reasoningOpenWhenCompleted += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
messageId: info.id,
|
||||
role: info.role,
|
||||
completedAt,
|
||||
latestToolTimestamp,
|
||||
latestReasoningTimestamp,
|
||||
latestPartTimestamp,
|
||||
completed_missing: !hasCompleted,
|
||||
completed_before_tool: completedBeforeTool,
|
||||
completed_before_reasoning: completedBeforeReasoning,
|
||||
running_tools_when_completed: Boolean(hasCompleted && hasRunningTool),
|
||||
reasoning_open_when_completed: Boolean(hasCompleted && reasoningIncomplete),
|
||||
has_tools: toolParts.length > 0,
|
||||
has_reasoning: reasoningParts.length > 0,
|
||||
};
|
||||
});
|
||||
|
||||
if (verbose) {
|
||||
console.table(rows.slice(0, maxTableRows));
|
||||
if (rows.length > maxTableRows) {
|
||||
console.log(`Displayed first ${maxTableRows} rows out of ${rows.length}.`);
|
||||
}
|
||||
console.log('[SUMMARY] Message completion timing:', summary);
|
||||
}
|
||||
|
||||
return { summary, rows };
|
||||
},
|
||||
|
||||
checkCompletionStatus() {
|
||||
const state = useSessionStore.getState();
|
||||
const currentSessionId = state.currentSessionId;
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('[ERROR] No active session');
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages = state.messages.get(currentSessionId) || [];
|
||||
const assistantMessages = messages.filter(m => m.info.role === 'assistant');
|
||||
|
||||
if (assistantMessages.length === 0) {
|
||||
console.log('[ERROR] No assistant messages');
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
||||
const stepFinishParts = lastMessage.parts.filter((p: any) => p.type === 'step-finish');
|
||||
const hasStopReason = lastMessage.parts.some((p: any) => p.type === 'step-finish' && p.reason === 'stop');
|
||||
|
||||
const timeInfo = lastMessage.info.time as any;
|
||||
const completedAt = timeInfo?.completed;
|
||||
const messageStatus = (lastMessage.info as any).status;
|
||||
const hasCompletedFlag = (typeof completedAt === 'number' && completedAt > 0) || messageStatus === 'completed';
|
||||
const messageIsComplete = Boolean(hasCompletedFlag && hasStopReason);
|
||||
|
||||
const messageStreamStates = state.messageStreamStates;
|
||||
const streamingMessageId = (lastMessage.info as { sessionID?: string }).sessionID
|
||||
? state.streamingMessageIds.get((lastMessage.info as { sessionID?: string }).sessionID as string) ?? null
|
||||
: null;
|
||||
const lifecycle = messageStreamStates.get(lastMessage.info.id);
|
||||
const isStreamingCandidate = lastMessage.info.id === streamingMessageId;
|
||||
|
||||
console.log('[SUMMARY] Completion Status:');
|
||||
console.log('Message ID:', lastMessage.info.id);
|
||||
console.log('time.completed:', completedAt, '(type:', typeof completedAt, ')');
|
||||
console.log('status:', messageStatus);
|
||||
console.log('hasCompletedFlag:', hasCompletedFlag);
|
||||
console.log('hasStopReason:', hasStopReason);
|
||||
console.log('messageIsComplete:', messageIsComplete);
|
||||
console.log('lifecycle phase:', lifecycle?.phase);
|
||||
console.log('isStreamingCandidate:', isStreamingCandidate);
|
||||
console.log('streamingMessageId:', streamingMessageId);
|
||||
console.log('Step-finish parts:', stepFinishParts);
|
||||
|
||||
return {
|
||||
messageId: lastMessage.info.id,
|
||||
completed: completedAt,
|
||||
status: messageStatus,
|
||||
hasCompletedFlag,
|
||||
hasStopReason,
|
||||
messageIsComplete,
|
||||
stepFinishParts,
|
||||
raw: lastMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__opencodeDebug = debugUtils;
|
||||
console.log('[DEBUG] OpenCode Debug Utils loaded! Use window.__opencodeDebug in console');
|
||||
console.log('Available commands:');
|
||||
console.log(' __opencodeDebug.getLastAssistantMessage() - Get last assistant message details');
|
||||
console.log(' __opencodeDebug.getAllMessages(truncate?) - List all messages (truncate=true for short preview)');
|
||||
console.log(' __opencodeDebug.truncateMessages(messages) - Truncate long fields in messages array');
|
||||
console.log(' __opencodeDebug.checkLastMessage() - Check if last message is problematic');
|
||||
console.log(' __opencodeDebug.findEmptyMessages() - Find all empty assistant messages');
|
||||
console.log(' __opencodeDebug.showRetryHelp() - Show instructions for handling empty responses');
|
||||
console.log(' __opencodeDebug.getStreamingState() - Get streaming state info');
|
||||
console.log(' __opencodeDebug.analyzeMessageCompletionConsistency(opts?) - Compare time.completed vs part timings');
|
||||
console.log(' __opencodeDebug.checkCompletionStatus() - Check completion status of last message');
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
try {
|
||||
const message = event.message || '';
|
||||
const source = event.filename || '';
|
||||
if (
|
||||
typeof message === 'string' &&
|
||||
message.includes("this._renderer.value.dimensions") &&
|
||||
/xterm/i.test(String(source))
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
} catch { /* ignored */ }
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user