fix: vscode runtime stability and cli connection
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useSessionStore } from '@openchamber/ui/stores/useSessionStore';
|
||||
import { useConfigStore } from '@openchamber/ui/stores/useConfigStore';
|
||||
import { useNavigation } from '../hooks/useNavigation';
|
||||
import { VSCodeHeader } from './VSCodeHeader';
|
||||
import { SimpleMessageRenderer } from './SimpleMessageRenderer';
|
||||
@@ -15,6 +16,7 @@ export function ChatPanel() {
|
||||
const sendMessage = useSessionStore((s) => s.sendMessage);
|
||||
const abortCurrentOperation = useSessionStore((s) => s.abortCurrentOperation);
|
||||
const streamingMessageIds = useSessionStore((s) => s.streamingMessageIds);
|
||||
const { currentProviderId, currentModelId, currentAgentName } = useConfigStore();
|
||||
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const [isSending, setIsSending] = React.useState(false);
|
||||
@@ -33,11 +35,17 @@ export function ChatPanel() {
|
||||
if (!inputValue.trim() || !currentSessionId || isSending) return;
|
||||
|
||||
const messageText = inputValue.trim();
|
||||
|
||||
if (!currentProviderId || !currentModelId) {
|
||||
console.warn('Missing provider or model selection for sendMessage');
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
await sendMessage(messageText);
|
||||
await sendMessage(messageText, currentProviderId, currentModelId, currentAgentName);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
setInputValue(messageText); // Restore input on error
|
||||
@@ -55,7 +63,7 @@ export function ChatPanel() {
|
||||
|
||||
const handleAbort = () => {
|
||||
if (currentSessionId) {
|
||||
abortCurrentOperation(currentSessionId);
|
||||
void abortCurrentOperation();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import type { Message, Part } from '@opencode-ai/sdk';
|
||||
import type { Message, Part, ToolPart } from '@opencode-ai/sdk';
|
||||
|
||||
interface SimpleMessageRendererProps {
|
||||
message: { info: Message; parts: Part[] };
|
||||
@@ -16,7 +16,7 @@ export function SimpleMessageRenderer({ message }: SimpleMessageRendererProps) {
|
||||
.join('\n');
|
||||
|
||||
// Check for tool calls
|
||||
const toolParts = parts.filter((part) => part.type === 'tool-invocation' || part.type === 'tool-result');
|
||||
const toolParts = parts.filter((part): part is ToolPart => part.type === 'tool');
|
||||
|
||||
return (
|
||||
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||
@@ -57,32 +57,20 @@ export function SimpleMessageRenderer({ message }: SimpleMessageRendererProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function ToolPartRenderer({ part }: { part: Part }) {
|
||||
if (part.type === 'tool-invocation') {
|
||||
const toolName = part.toolInvocation?.toolName || 'tool';
|
||||
const state = part.toolInvocation?.state || 'pending';
|
||||
function ToolPartRenderer({ part }: { part: ToolPart }) {
|
||||
const toolName = typeof part.tool === 'string' ? part.tool : 'tool';
|
||||
const status = typeof part.state?.status === 'string' ? part.state.status : 'pending';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{state === 'pending' || state === 'streaming' ? (
|
||||
<span className="inline-block w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : state === 'result' ? (
|
||||
<span className="text-green-500">✓</span>
|
||||
) : (
|
||||
<span className="text-red-500">✗</span>
|
||||
)}
|
||||
<span className="font-mono">{toolName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === 'tool-result') {
|
||||
return (
|
||||
<div className="text-xs text-muted-foreground font-mono truncate">
|
||||
Result: {typeof part.result === 'string' ? part.result.slice(0, 50) : '...'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{status === 'running' || status === 'pending' ? (
|
||||
<span className="inline-block w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : status === 'completed' ? (
|
||||
<span className="text-green-500">✓</span>
|
||||
) : (
|
||||
<span className="text-red-500">✗</span>
|
||||
)}
|
||||
<span className="font-mono">{toolName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ declare global {
|
||||
workspaceFolder: string;
|
||||
theme: string;
|
||||
connectionStatus: string;
|
||||
cliAvailable?: boolean;
|
||||
};
|
||||
__OPENCHAMBER_VSCODE_THEME__?: VSCodeThemePayload['theme'];
|
||||
__OPENCHAMBER_VSCODE_SHIKI_THEMES__?: { light?: Record<string, unknown>; dark?: Record<string, unknown> } | null;
|
||||
__OPENCHAMBER_CONNECTION__?: { status: ConnectionStatus; error?: string };
|
||||
__OPENCHAMBER_CONNECTION__?: { status: ConnectionStatus; error?: string; cliAvailable?: boolean };
|
||||
__OPENCHAMBER_HOME__?: string;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +34,8 @@ window.__OPENCHAMBER_RUNTIME_APIS__ = createVSCodeAPIs();
|
||||
|
||||
const bootstrapConnectionStatus = () => {
|
||||
const initialStatus = (window.__VSCODE_CONFIG__?.connectionStatus as ConnectionStatus | undefined) || 'connecting';
|
||||
window.__OPENCHAMBER_CONNECTION__ = { status: initialStatus };
|
||||
const cliAvailable = window.__VSCODE_CONFIG__?.cliAvailable ?? true;
|
||||
window.__OPENCHAMBER_CONNECTION__ = { status: initialStatus, cliAvailable };
|
||||
};
|
||||
|
||||
bootstrapConnectionStatus();
|
||||
@@ -43,8 +45,18 @@ const handleConnectionMessage = (event: MessageEvent) => {
|
||||
if (msg?.type === 'connectionStatus') {
|
||||
const payload: ConnectionStatus = msg.status;
|
||||
const error: string | undefined = msg.error;
|
||||
window.__OPENCHAMBER_CONNECTION__ = { status: payload, error };
|
||||
const prevCliAvailable = window.__OPENCHAMBER_CONNECTION__?.cliAvailable ?? true;
|
||||
window.__OPENCHAMBER_CONNECTION__ = { status: payload, error, cliAvailable: prevCliAvailable };
|
||||
window.dispatchEvent(new CustomEvent('openchamber:connection-status', { detail: { status: payload, error } }));
|
||||
|
||||
// Hide loading screen when connected
|
||||
if (payload === 'connected') {
|
||||
const loadingEl = document.getElementById('initial-loading');
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add('fade-out');
|
||||
setTimeout(() => loadingEl.remove(), 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,14 +146,55 @@ const normalizeUrl = (input: string | URL) => {
|
||||
}
|
||||
};
|
||||
|
||||
const apiBaseUrl = window.__VSCODE_CONFIG__?.apiUrl?.replace(/\/+$/, '') || 'http://localhost:47339';
|
||||
// API URL may be empty during initial load while port is being detected
|
||||
// The extension will broadcast the URL once it's known
|
||||
let apiBaseUrl = window.__VSCODE_CONFIG__?.apiUrl?.replace(/\/+$/, '') || '';
|
||||
|
||||
// Promise that resolves when API URL is available
|
||||
let apiUrlResolver: ((url: string) => void) | null = null;
|
||||
const apiUrlPromise = apiBaseUrl
|
||||
? Promise.resolve(apiBaseUrl)
|
||||
: new Promise<string>((resolve) => { apiUrlResolver = resolve; });
|
||||
|
||||
// Listen for API URL updates from extension
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
const msg = event.data;
|
||||
if (msg?.type === 'apiUrlUpdate' && typeof msg.url === 'string') {
|
||||
const newUrl = msg.url.replace(/\/+$/, '');
|
||||
apiBaseUrl = newUrl;
|
||||
console.log('[OpenChamber] API URL updated:', apiBaseUrl);
|
||||
// Resolve the promise if waiting
|
||||
if (apiUrlResolver) {
|
||||
apiUrlResolver(newUrl);
|
||||
apiUrlResolver = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to wait for API URL with timeout
|
||||
const waitForApiUrl = async (timeoutMs = 15000): Promise<string> => {
|
||||
if (apiBaseUrl) return apiBaseUrl;
|
||||
|
||||
const timeout = new Promise<string>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout waiting for API URL')), timeoutMs)
|
||||
);
|
||||
|
||||
return Promise.race([apiUrlPromise, timeout]);
|
||||
};
|
||||
|
||||
const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Health endpoints: always return OK to avoid blocking VS Code UX
|
||||
// Health endpoints: reflect actual connection status
|
||||
if (pathname === '/health' || pathname === '/api/health') {
|
||||
return new Response(JSON.stringify({ status: 'ok', isOpenCodeReady: true }), {
|
||||
const connectionStatus = window.__OPENCHAMBER_CONNECTION__?.status;
|
||||
const isReady = connectionStatus === 'connected';
|
||||
const cliAvailable = window.__OPENCHAMBER_CONNECTION__?.cliAvailable ?? true;
|
||||
return new Response(JSON.stringify({
|
||||
status: isReady ? 'ok' : 'connecting',
|
||||
isOpenCodeReady: isReady,
|
||||
cliAvailable,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
@@ -258,7 +311,14 @@ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const pathname = targetUrl?.pathname || '';
|
||||
const normalizedPathname = pathname.replace(/\/+/, '/');
|
||||
if (targetUrl && normalizedPathname === '/health') {
|
||||
return new Response(JSON.stringify({ status: 'ok', isOpenCodeReady: true }), {
|
||||
const connectionStatus = window.__OPENCHAMBER_CONNECTION__?.status;
|
||||
const isReady = connectionStatus === 'connected';
|
||||
const cliAvailable = window.__OPENCHAMBER_CONNECTION__?.cliAvailable ?? true;
|
||||
return new Response(JSON.stringify({
|
||||
status: isReady ? 'ok' : 'connecting',
|
||||
isOpenCodeReady: isReady,
|
||||
cliAvailable,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
@@ -270,9 +330,12 @@ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
return localResponse;
|
||||
}
|
||||
|
||||
// Wait for API URL to be available before making requests
|
||||
const baseUrl = await waitForApiUrl();
|
||||
|
||||
const rewritten = new URL(targetUrl.href);
|
||||
rewritten.pathname = targetUrl.pathname.replace(/^\/api/, '');
|
||||
const fetchTarget = `${apiBaseUrl}${rewritten.pathname}${rewritten.search}`;
|
||||
const fetchTarget = `${baseUrl}${rewritten.pathname}${rewritten.search}`;
|
||||
|
||||
if (input instanceof Request) {
|
||||
const cloned = input.clone();
|
||||
|
||||
Reference in New Issue
Block a user