2025-12-07 19:32:53 +02:00
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
2025-12-24 03:32:24 +02:00
|
|
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
2026-01-16 14:43:53 +02:00
|
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
2025-12-24 03:32:24 +02:00
|
|
|
import { opencodeClient } from '@/lib/opencode/client';
|
|
|
|
|
import { checkIsGitRepository } from '@/lib/gitApi';
|
2025-12-25 03:33:24 +02:00
|
|
|
import { streamDebugEnabled } from '@/stores/utils/streamDebug';
|
2026-02-20 14:50:26 +02:00
|
|
|
import { copyTextToClipboard as copyPlainTextToClipboard } from '@/lib/clipboard';
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2025-12-24 03:32:24 +02:00
|
|
|
async getAppStatus() {
|
|
|
|
|
const directoryState = useDirectoryStore.getState();
|
|
|
|
|
const sessionState = useSessionStore.getState();
|
2026-01-16 14:43:53 +02:00
|
|
|
const projectsState = useProjectsStore.getState();
|
2025-12-24 03:32:24 +02:00
|
|
|
const currentDirectory = directoryState.currentDirectory || null;
|
|
|
|
|
const opencodeDirectory = opencodeClient.getDirectory() ?? null;
|
|
|
|
|
|
|
|
|
|
const sessions = sessionState.sessions || [];
|
|
|
|
|
const sessionDirectories = new Set<string>();
|
|
|
|
|
const sessionDirectoryCounts: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
sessions.forEach((session) => {
|
|
|
|
|
const directory = (session as { directory?: string | null }).directory;
|
|
|
|
|
if (typeof directory === 'string' && directory.trim().length > 0) {
|
|
|
|
|
sessionDirectories.add(directory);
|
|
|
|
|
sessionDirectoryCounts[directory] = (sessionDirectoryCounts[directory] ?? 0) + 1;
|
|
|
|
|
} else {
|
|
|
|
|
sessionDirectoryCounts['(none)'] = (sessionDirectoryCounts['(none)'] ?? 0) + 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const localStorageSnapshot = (() => {
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
return { available: false };
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return {
|
|
|
|
|
available: true,
|
|
|
|
|
lastDirectory: window.localStorage.getItem('lastDirectory'),
|
|
|
|
|
homeDirectory: window.localStorage.getItem('homeDirectory'),
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
available: false,
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
const runtimeApis = typeof window !== 'undefined'
|
|
|
|
|
? (window as any).__OPENCHAMBER_RUNTIME_APIS__
|
|
|
|
|
: null;
|
2026-02-05 01:59:49 +02:00
|
|
|
const isTauriShell = typeof window !== 'undefined' && Boolean((window as any).__TAURI__);
|
2025-12-24 03:32:24 +02:00
|
|
|
|
|
|
|
|
const safeJson = async (resp: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
return await resp.json();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const safeText = async (resp: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
return await resp.text();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const safeFetchJson = async (url: string): Promise<unknown> => {
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(url);
|
|
|
|
|
return resp.ok ? await safeJson(resp) : { status: resp.status };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let pathInfo: unknown = null;
|
|
|
|
|
let projectInfo: unknown = null;
|
|
|
|
|
let settingsInfo: unknown = null;
|
|
|
|
|
let opencodeHealth: unknown = null;
|
|
|
|
|
|
|
|
|
|
const pathUrl = currentDirectory
|
|
|
|
|
? `/api/path?directory=${encodeURIComponent(currentDirectory)}`
|
|
|
|
|
: '/api/path';
|
|
|
|
|
pathInfo = await safeFetchJson(pathUrl);
|
|
|
|
|
|
|
|
|
|
const projectUrl = currentDirectory
|
|
|
|
|
? `/api/project/current?directory=${encodeURIComponent(currentDirectory)}`
|
|
|
|
|
: '/api/project/current';
|
|
|
|
|
projectInfo = await safeFetchJson(projectUrl);
|
|
|
|
|
|
|
|
|
|
settingsInfo = await safeFetchJson('/api/config/settings');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch('/api/health');
|
|
|
|
|
const contentType = resp.headers.get('content-type') || '';
|
|
|
|
|
const body = await safeText(resp);
|
2026-02-17 18:01:57 +02:00
|
|
|
const isJson = contentType.toLowerCase().includes('application/json');
|
|
|
|
|
let parsed: Record<string, unknown> | null = null;
|
|
|
|
|
if (isJson && body) {
|
|
|
|
|
try {
|
|
|
|
|
const candidate = JSON.parse(body) as unknown;
|
|
|
|
|
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
|
|
|
parsed = candidate as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
parsed = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-24 03:32:24 +02:00
|
|
|
opencodeHealth = {
|
|
|
|
|
status: resp.status,
|
|
|
|
|
ok: resp.ok,
|
|
|
|
|
contentType,
|
2026-02-17 18:01:57 +02:00
|
|
|
type: isJson ? 'json' : 'html',
|
|
|
|
|
openCodePort: parsed?.openCodePort ?? null,
|
|
|
|
|
openCodeRunning: parsed?.openCodeRunning ?? null,
|
|
|
|
|
openCodeSecureConnection: parsed?.openCodeSecureConnection ?? null,
|
|
|
|
|
openCodeAuthSource: parsed?.openCodeAuthSource ?? null,
|
|
|
|
|
isOpenCodeReady: parsed?.isOpenCodeReady ?? null,
|
|
|
|
|
lastOpenCodeError: parsed?.lastOpenCodeError ?? null,
|
2025-12-24 03:32:24 +02:00
|
|
|
preview: body ? body.slice(0, 120) : null,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
opencodeHealth = { error: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let gitCheck: { isGitRepo: boolean | null; error?: string } = { isGitRepo: null };
|
|
|
|
|
if (currentDirectory) {
|
|
|
|
|
try {
|
|
|
|
|
gitCheck.isGitRepo = await checkIsGitRepository(currentDirectory);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
gitCheck = {
|
|
|
|
|
isGitRepo: null,
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
const projectSamples = projectsState.projects.map((project) => ({
|
|
|
|
|
id: project.id,
|
|
|
|
|
path: project.path,
|
|
|
|
|
label: project.label,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-12-24 03:32:24 +02:00
|
|
|
const report = {
|
|
|
|
|
runtime: {
|
2026-01-16 14:43:53 +02:00
|
|
|
platform: runtimeApis?.runtime?.platform ?? null,
|
2026-02-05 01:59:49 +02:00
|
|
|
isDesktop: isTauriShell,
|
2026-01-16 14:43:53 +02:00
|
|
|
isVSCode: Boolean(runtimeApis?.runtime?.isVSCode),
|
2025-12-24 03:32:24 +02:00
|
|
|
hasRuntimeApis: Boolean(runtimeApis),
|
2026-02-05 01:59:49 +02:00
|
|
|
desktopServerOrigin: null,
|
2025-12-24 03:32:24 +02:00
|
|
|
},
|
|
|
|
|
location: typeof window !== 'undefined'
|
|
|
|
|
? {
|
|
|
|
|
href: window.location?.href ?? null,
|
|
|
|
|
origin: window.location?.origin ?? null,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
directories: {
|
|
|
|
|
currentDirectory,
|
|
|
|
|
opencodeDirectory: (pathInfo as { directory?: string; worktree?: string } | null)?.directory
|
|
|
|
|
|| (pathInfo as { worktree?: string } | null)?.worktree
|
|
|
|
|
|| opencodeDirectory,
|
|
|
|
|
homeDirectory: directoryState.homeDirectory || null,
|
|
|
|
|
isHomeReady: directoryState.isHomeReady,
|
|
|
|
|
hasPersistedDirectory: directoryState.hasPersistedDirectory,
|
|
|
|
|
isSwitchingDirectory: directoryState.isSwitchingDirectory,
|
|
|
|
|
},
|
2026-01-16 14:43:53 +02:00
|
|
|
projects: {
|
|
|
|
|
total: projectsState.projects.length,
|
|
|
|
|
activeProjectId: projectsState.activeProjectId,
|
|
|
|
|
samples: projectSamples,
|
|
|
|
|
},
|
2025-12-24 03:32:24 +02:00
|
|
|
sessions: {
|
|
|
|
|
total: sessions.length,
|
|
|
|
|
currentSessionId: sessionState.currentSessionId,
|
|
|
|
|
lastLoadedDirectory: sessionState.lastLoadedDirectory,
|
|
|
|
|
uniqueDirectories: sessionDirectories.size,
|
|
|
|
|
directorySamples: Array.from(sessionDirectories).slice(0, 5),
|
|
|
|
|
directoryCounts: sessionDirectoryCounts,
|
|
|
|
|
},
|
|
|
|
|
worktrees: {
|
|
|
|
|
available: sessionState.availableWorktrees.length,
|
|
|
|
|
metadataEntries: sessionState.worktreeMetadata.size,
|
|
|
|
|
},
|
|
|
|
|
git: gitCheck,
|
|
|
|
|
localStorage: localStorageSnapshot,
|
|
|
|
|
opencode: {
|
|
|
|
|
pathInfo,
|
|
|
|
|
projectInfo,
|
|
|
|
|
health: opencodeHealth,
|
|
|
|
|
},
|
|
|
|
|
openchamber: {
|
|
|
|
|
settingsInfo,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('[DEBUG] App status snapshot:', report);
|
|
|
|
|
return report;
|
|
|
|
|
},
|
|
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
async buildDiagnosticsReport() {
|
|
|
|
|
const report = await this.getAppStatus();
|
|
|
|
|
return JSON.stringify(report, null, 2);
|
|
|
|
|
},
|
|
|
|
|
|
2026-02-20 14:50:26 +02:00
|
|
|
async copyTextToClipboard(text: string) {
|
|
|
|
|
return copyPlainTextToClipboard(text);
|
|
|
|
|
},
|
|
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
async copyDiagnosticsReport() {
|
|
|
|
|
const report = await this.buildDiagnosticsReport();
|
2026-02-20 14:50:26 +02:00
|
|
|
const result = await this.copyTextToClipboard(report);
|
|
|
|
|
return { ...result, report } as const;
|
2026-01-16 14:43:53 +02:00
|
|
|
},
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
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');
|
2025-12-27 15:51:28 +02:00
|
|
|
const hasStopReason = (lastMessage.info as { finish?: string }).finish === 'stop';
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
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;
|
2025-12-25 03:33:24 +02:00
|
|
|
if (streamDebugEnabled()) {
|
|
|
|
|
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.getAppStatus() - Show app status snapshot');
|
|
|
|
|
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');
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
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 */ }
|
|
|
|
|
});
|
|
|
|
|
}
|