feat: vscode extension (#59)
* feat: add initial VS Code extension plan and implementation tasks * feat(vscode): added initial version of an Openchamber VSCode extension * feat(vscode): enhance VS Code extension with theme integration and session management * feat(vscode): implement connection status handling and overlay in VSCode layout * feat: move extension to secondary sidebar * chore: upgrade @opencode-ai/sdk to 1.0.150 * vscode: editor bridge, file picker, click-to-open in tool parts * vscode: layout session lifecycle, theme sync, typography overrides * ui: compact mode for vscode, model search, autocomplete width fixes * perf: scroll force flag, raf placeholder, git polling backoff * ui: tool output styling, markdown code block fix, gitignore * refactor: update typography handling for VSCode runtime, remove unused styles * docs: update README with VS Code extension details and add extension image * docs: update changelog with new features and performance improvements
This commit is contained in:
committed by
GitHub
parent
610ccf4c62
commit
bb72c0fb0c
@@ -0,0 +1,183 @@
|
||||
import { create } from 'zustand';
|
||||
import { createOpencodeClient, type OpencodeClient } from '@opencode-ai/sdk';
|
||||
import type { Session, Message, Part } from '@opencode-ai/sdk';
|
||||
|
||||
const getApiUrl = () => window.__VSCODE_CONFIG__?.apiUrl || 'http://localhost:47339';
|
||||
const getWorkspaceFolder = () => window.__VSCODE_CONFIG__?.workspaceFolder || '';
|
||||
|
||||
interface MessageRecord {
|
||||
info: Message;
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
// Client
|
||||
client: OpencodeClient | null;
|
||||
isConnected: boolean;
|
||||
|
||||
// Sessions
|
||||
sessions: Session[];
|
||||
currentSessionId: string | null;
|
||||
isLoadingSessions: boolean;
|
||||
|
||||
// Messages
|
||||
messages: Map<string, MessageRecord[]>;
|
||||
isLoadingMessages: boolean;
|
||||
isSending: boolean;
|
||||
streamingSessionId: string | null;
|
||||
|
||||
// Actions
|
||||
initialize: () => Promise<void>;
|
||||
loadSessions: () => Promise<void>;
|
||||
createSession: () => Promise<string | null>;
|
||||
selectSession: (sessionId: string) => Promise<void>;
|
||||
loadMessages: (sessionId: string) => Promise<void>;
|
||||
sendMessage: (content: string) => Promise<void>;
|
||||
abortMessage: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>((set, get) => ({
|
||||
client: null,
|
||||
isConnected: false,
|
||||
sessions: [],
|
||||
currentSessionId: null,
|
||||
isLoadingSessions: false,
|
||||
messages: new Map(),
|
||||
isLoadingMessages: false,
|
||||
isSending: false,
|
||||
streamingSessionId: null,
|
||||
|
||||
initialize: async () => {
|
||||
const apiUrl = getApiUrl();
|
||||
const client = createOpencodeClient({ baseUrl: apiUrl });
|
||||
|
||||
// Test connection
|
||||
try {
|
||||
await client.session.list({ query: { directory: getWorkspaceFolder() } });
|
||||
set({ client, isConnected: true });
|
||||
await get().loadSessions();
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to OpenCode API:', error);
|
||||
set({ client, isConnected: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadSessions: async () => {
|
||||
const { client } = get();
|
||||
if (!client) return;
|
||||
|
||||
set({ isLoadingSessions: true });
|
||||
try {
|
||||
const response = await client.session.list({ query: { directory: getWorkspaceFolder() } });
|
||||
const sessionsArray = Array.isArray(response.data) ? response.data : [];
|
||||
const sessions = sessionsArray.sort(
|
||||
(a, b) => (b.time?.created || 0) - (a.time?.created || 0)
|
||||
);
|
||||
set({ sessions, isLoadingSessions: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to load sessions:', error);
|
||||
set({ isLoadingSessions: false });
|
||||
}
|
||||
},
|
||||
|
||||
createSession: async () => {
|
||||
const { client } = get();
|
||||
if (!client) return null;
|
||||
|
||||
try {
|
||||
const response = await client.session.create({ query: { directory: getWorkspaceFolder() }, body: {} });
|
||||
const session = response.data;
|
||||
if (!session) throw new Error('No session returned');
|
||||
await get().loadSessions();
|
||||
set({ currentSessionId: session.id });
|
||||
return session.id;
|
||||
} catch (error) {
|
||||
console.error('Failed to create session:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
selectSession: async (sessionId: string) => {
|
||||
set({ currentSessionId: sessionId });
|
||||
await get().loadMessages(sessionId);
|
||||
},
|
||||
|
||||
loadMessages: async (sessionId: string) => {
|
||||
const { client, messages } = get();
|
||||
if (!client) return;
|
||||
|
||||
set({ isLoadingMessages: true });
|
||||
try {
|
||||
const response = await client.session.messages({
|
||||
path: { id: sessionId },
|
||||
query: { directory: getWorkspaceFolder() }
|
||||
});
|
||||
const messageRecords: MessageRecord[] = (response.data || []).map((msg) => ({
|
||||
info: msg.info,
|
||||
parts: msg.parts || [],
|
||||
}));
|
||||
|
||||
const newMessages = new Map(messages);
|
||||
newMessages.set(sessionId, messageRecords);
|
||||
set({ messages: newMessages, isLoadingMessages: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages:', error);
|
||||
set({ isLoadingMessages: false });
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (content: string) => {
|
||||
const { client, currentSessionId, messages } = get();
|
||||
if (!client || !currentSessionId) return;
|
||||
|
||||
set({ isSending: true, streamingSessionId: currentSessionId });
|
||||
|
||||
try {
|
||||
// Add user message optimistically
|
||||
const userMessage: MessageRecord = {
|
||||
info: {
|
||||
id: `temp-${Date.now()}`,
|
||||
sessionId: currentSessionId,
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: content }],
|
||||
time: { created: Date.now() },
|
||||
} as Message,
|
||||
parts: [{ type: 'text', text: content }],
|
||||
};
|
||||
|
||||
const currentMessages = messages.get(currentSessionId) || [];
|
||||
const newMessages = new Map(messages);
|
||||
newMessages.set(currentSessionId, [...currentMessages, userMessage]);
|
||||
set({ messages: newMessages });
|
||||
|
||||
// Send message via session.prompt
|
||||
await client.session.prompt({
|
||||
path: { id: currentSessionId },
|
||||
query: { directory: getWorkspaceFolder() },
|
||||
body: {
|
||||
parts: [{ type: 'text', text: content }],
|
||||
},
|
||||
});
|
||||
|
||||
// Reload messages to get the actual response
|
||||
await get().loadMessages(currentSessionId);
|
||||
await get().loadSessions(); // Update session title if changed
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
} finally {
|
||||
set({ isSending: false, streamingSessionId: null });
|
||||
}
|
||||
},
|
||||
|
||||
abortMessage: async () => {
|
||||
const { client, currentSessionId } = get();
|
||||
if (!client || !currentSessionId) return;
|
||||
|
||||
try {
|
||||
await client.session.abort({ path: { id: currentSessionId } });
|
||||
} catch (error) {
|
||||
console.error('Failed to abort:', error);
|
||||
}
|
||||
set({ isSending: false, streamingSessionId: null });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user