import React, { createContext, useContext, useReducer, useEffect, useCallback, useRef } from 'react';
import { ChatState, ChatAction, ExtensionMessage } from './types';
import { chatReducer, initialState } from './reducer';
import { postMessage, getVSCodeApi } from '../vscode';
interface ChatContextValue {
state: ChatState;
dispatch: React.Dispatch<ChatAction>;
send: (text: string) => void;
stop: () => void;
newConversation: () => void;
selectModel: (provider: string, model?: string) => void;
loadSession: (sessionId: string, projectHash?: string) => void;
openSidebar: () => void;
openSessionInTab: (sessionId?: string, projectHash?: string) => void;
renameSession: (session: { id: string; project_hash?: string; name?: string; title?: string }) => void;
deleteSession: (session: { id: string; project_hash?: string; name?: string; title?: string }) => void;
deleteSessions: (sessions: Array<{ id: string; project_hash?: string; name?: string }>) => void;
startLogin: () => void;
cancelLogin: () => void;
setupCodingPlan: () => void;
refreshSetupState: () => void;
setDefaultProvider: (name: string) => void;
}
const ChatContext = createContext<ChatContextValue | null>(null);
export function useChatContext(): ChatContextValue {
const ctx = useContext(ChatContext);
if (!ctx) throw new Error('useChatContext must be used inside <ChatProvider>');
return ctx;
}
let _toolIdCounter = 0;
export function ChatProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(chatReducer, initialState);
const stateRef = useRef(state);
stateRef.current = state;
useEffect(() => {
function handleMessage(event: MessageEvent<ExtensionMessage>) {
const msg = event.data;
switch (msg.type) {
case 'init':
dispatch({
type: 'INIT',
generating: msg.generating,
currentModel: msg.currentModel,
viewMode: msg.viewMode,
activeSessionId: msg.activeSessionId,
projectHash: msg.projectHash,
isSessionList: msg.isSessionList,
});
if (msg.activeSessionId) {
getVSCodeApi().setState({ sessionId: msg.activeSessionId, projectHash: msg.projectHash });
}
break;
case 'userMessage':
dispatch({ type: 'ADD_USER_MESSAGE', text: msg.text });
break;
case 'queuedMessageSent':
dispatch({ type: 'SEND_QUEUED_MESSAGE', id: msg.id });
break;
case 'assistantMessage':
dispatch({ type: 'ADD_ASSISTANT_MESSAGE', text: msg.text });
break;
case 'generationStarted':
dispatch({ type: 'START_GENERATION' });
break;
case 'text':
dispatch({ type: 'APPEND_TEXT', content: msg.content });
break;
case 'toolBatchStart':
dispatch({ type: 'TOOL_BATCH_START', calls: msg.calls });
break;
case 'toolStart':
dispatch({
type: 'TOOL_START',
id: msg.id || `tool-${++_toolIdCounter}`,
name: msg.name,
args: msg.args,
});
break;
case 'toolResult':
{
const msgs = stateRef.current.messages;
const last = msgs[msgs.length - 1];
const targetTool = msg.id
? last?.toolCalls?.find((t) => t.id === msg.id)
: last?.toolCalls?.findLast((t) => t.status === 'running');
if (targetTool) {
dispatch({
type: 'TOOL_RESULT',
id: targetTool.id,
name: msg.name,
output: msg.output,
success: msg.success,
durationMs: msg.durationMs,
});
}
}
break;
case 'tokens':
dispatch({ type: 'SET_TOKENS', prompt: msg.prompt, completion: msg.completion, total: msg.total });
break;
case 'done':
dispatch({ type: 'GENERATION_DONE', tokens: msg.tokens });
if (msg.sessionId) {
dispatch({ type: 'SET_ACTIVE_SESSION', sessionId: msg.sessionId });
}
break;
case 'stopped':
case 'generationStopped':
dispatch({ type: 'GENERATION_STOPPED' });
break;
case 'error':
dispatch({ type: 'GENERATION_ERROR', message: msg.message });
break;
case 'clearChat':
dispatch({ type: 'CLEAR_CHAT' });
break;
case 'resumeStreaming':
dispatch({ type: 'RESUME_STREAMING' });
break;
case 'sessions':
dispatch({ type: 'SET_SESSIONS', sessions: msg.sessions });
break;
case 'sessionSelected':
dispatch({ type: 'SET_ACTIVE_SESSION', sessionId: msg.sessionId, projectHash: msg.projectHash });
break;
case 'models':
dispatch({ type: 'SET_MODELS', models: msg.models });
break;
case 'providers':
dispatch({ type: 'SET_PROVIDERS', providers: msg.providers, defaultProvider: msg.defaultProvider });
break;
case 'authStatus':
dispatch({ type: 'SET_AUTH', auth: msg.auth });
break;
case 'setupState':
dispatch({
type: 'SET_SETUP_STATE',
auth: msg.auth,
providers: msg.providers,
defaultProvider: msg.defaultProvider,
currentModel: msg.currentModel,
setupRequired: msg.setupRequired,
});
break;
case 'loginStarted':
dispatch({ type: 'SET_SETUP_STATUS', status: 'Waiting for browser authorization...', loginUrl: msg.url });
break;
case 'loginPending':
dispatch({ type: 'SET_SETUP_STATUS', status: 'Waiting for browser authorization...' });
break;
case 'loginAuthorized':
dispatch({ type: 'SET_SETUP_STATUS', status: 'Signed in. Sync CodingPlan models or add a provider.' });
break;
case 'setupWorking':
dispatch({ type: 'SET_SETUP_STATUS', status: msg.message });
break;
case 'codingPlanResult':
dispatch({
type: 'SET_SETUP_STATUS',
status: msg.result.report_text,
});
break;
case 'setupError':
dispatch({ type: 'SET_SETUP_STATUS', error: msg.message });
break;
case 'sessionMessages':
dispatch({ type: 'LOAD_SESSION_MESSAGES', messages: msg.messages });
break;
case 'context':
dispatch({
type: 'ADD_CONTEXT_FILE',
file: {
path: msg.filePath,
fileName: msg.fileName,
language: msg.language,
selection: msg.selection,
startLine: msg.startLine,
endLine: msg.endLine,
type: msg.selection ? 'selection' : 'file',
},
});
break;
case 'permissionRequest':
dispatch({
type: 'PERMISSION_REQUEST',
id: msg.id,
toolName: msg.toolName,
args: msg.args,
isDestructive: msg.isDestructive,
});
break;
case 'focusInput':
break;
}
}
window.addEventListener('message', handleMessage);
postMessage({ type: 'ready' });
return () => window.removeEventListener('message', handleMessage);
}, []);
const send = useCallback(
(text: string) => {
const state = stateRef.current;
const ctx = stateRef.current.contextFiles.length > 0
? stateRef.current.contextFiles.map((f) => ({
path: f.path,
type: f.type,
fileName: f.fileName,
language: f.language,
selection: f.selection,
startLine: f.startLine,
endLine: f.endLine,
}))
: undefined;
const contextFiles = state.contextFiles;
const isQueued = state.isGenerating;
const clientMessageId = `queued-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
if (isQueued) {
dispatch({ type: 'ADD_QUEUED_MESSAGE', id: clientMessageId, text, contextFiles });
} else {
dispatch({ type: 'ADD_USER_MESSAGE', text, contextFiles });
}
postMessage({ type: 'send', text, context: ctx, clientMessageId: isQueued ? clientMessageId : undefined, sessionId: state.activeSessionId });
dispatch({ type: 'CLEAR_CONTEXT' });
},
[],
);
const stop = useCallback(() => {
postMessage({ type: 'stop' });
}, []);
const newConversation = useCallback(() => {
postMessage({ type: 'newConversation' });
}, []);
const selectModel = useCallback((provider: string, model?: string) => {
dispatch({ type: 'SET_CURRENT_PROVIDER', provider, model });
postMessage({ type: 'selectModel', provider, model });
}, []);
const loadSession = useCallback((sessionId: string, projectHash?: string) => {
postMessage({ type: 'loadSession', sessionId, projectHash });
}, []);
const openSidebar = useCallback(() => {
postMessage({ type: 'openSidebar' });
}, []);
const openSessionInTab = useCallback((sessionId?: string, projectHash?: string) => {
postMessage({ type: 'openSessionInTab', sessionId, projectHash });
}, []);
const renameSession = useCallback((session: { id: string; project_hash?: string; name?: string; title?: string }) => {
postMessage({
type: 'renameSession',
sessionId: session.id,
projectHash: session.project_hash,
name: session.name || session.title || '',
});
}, []);
const deleteSession = useCallback((session: { id: string; project_hash?: string; name?: string; title?: string }) => {
postMessage({
type: 'deleteSession',
sessionId: session.id,
projectHash: session.project_hash,
name: session.name || session.title || '',
});
}, []);
const deleteSessions = useCallback((sessions: Array<{ id: string; project_hash?: string; name?: string }>) => {
postMessage({
type: 'deleteSessions',
sessions: sessions.map((s) => ({
sessionId: s.id,
projectHash: s.project_hash,
name: s.name || '',
})),
});
}, []);
const startLogin = useCallback(() => {
postMessage({ type: 'authLoginStart' });
}, []);
const cancelLogin = useCallback(() => {
postMessage({ type: 'authLoginCancel' });
}, []);
const setupCodingPlan = useCallback(() => {
postMessage({ type: 'codingPlanSetup' });
}, []);
const refreshSetupState = useCallback(() => {
postMessage({ type: 'refreshSetupState' });
}, []);
const setDefaultProvider = useCallback((name: string) => {
postMessage({ type: 'providerSetDefault', name });
}, []);
const value: ChatContextValue = {
state,
dispatch,
send,
stop,
openSidebar,
newConversation,
selectModel,
loadSession,
openSessionInTab,
renameSession,
deleteSession,
deleteSessions,
startLogin,
cancelLogin,
setupCodingPlan,
refreshSetupState,
setDefaultProvider,
};
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
}