feat(vscode): add session editor panel (#146)

Add command to open active session in editor panel.
Introduce editor view mode for displaying sessions alongside files.
Update toolbar navigation for editor and sidebar modes.
This commit is contained in:
Bohdan Triapitsyn
2026-01-14 17:13:47 +02:00
committed by GitHub
parent c52a5f27a2
commit 032229c7a2
7 changed files with 692 additions and 9 deletions
@@ -6,6 +6,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { McpDropdown } from '@/components/mcp/McpDropdown';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { RiAddLine, RiArrowLeftLine, RiRobot2Line, RiSettings3Line } from '@remixicon/react';
// Width threshold for mobile vs desktop layout in settings
@@ -14,11 +15,41 @@ const MOBILE_WIDTH_THRESHOLD = 550;
type VSCodeView = 'sessions' | 'chat' | 'settings';
export const VSCodeLayout: React.FC = () => {
const runtimeApis = useRuntimeAPIs();
const viewMode = React.useMemo<'sidebar' | 'editor'>(() => {
const configured =
typeof window !== 'undefined'
? (window as unknown as { __VSCODE_CONFIG__?: { viewMode?: unknown } }).__VSCODE_CONFIG__?.viewMode
: null;
return configured === 'editor' ? 'editor' : 'sidebar';
}, []);
const initialSessionId = React.useMemo<string | null>(() => {
const configured =
typeof window !== 'undefined'
? (window as unknown as { __VSCODE_CONFIG__?: { initialSessionId?: unknown } }).__VSCODE_CONFIG__?.initialSessionId
: null;
if (typeof configured === 'string' && configured.trim().length > 0) {
return configured.trim();
}
return null;
}, []);
const hasAppliedInitialSession = React.useRef(false);
const [currentView, setCurrentView] = React.useState<VSCodeView>('chat');
const [containerWidth, setContainerWidth] = React.useState<number>(0);
const containerRef = React.useRef<HTMLDivElement>(null);
const currentSessionId = useSessionStore((state) => state.currentSessionId);
const sessions = useSessionStore((state) => state.sessions);
const activeSessionTitle = React.useMemo(() => {
if (!currentSessionId) {
return null;
}
return sessions.find((session) => session.id === currentSessionId)?.title || 'Session';
}, [currentSessionId, sessions]);
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
const [connectionStatus, setConnectionStatus] = React.useState<'connecting' | 'connected' | 'error' | 'disconnected'>(
@@ -43,17 +74,31 @@ export const VSCodeLayout: React.FC = () => {
}
}, [currentSessionId]);
React.useEffect(() => {
const vscodeApi = runtimeApis.vscode;
if (!vscodeApi) {
return;
}
void vscodeApi.executeCommand('openchamber.setActiveSession', currentSessionId, activeSessionTitle);
}, [activeSessionTitle, currentSessionId, runtimeApis.vscode]);
// If the active session disappears (e.g., deleted), show a new chat view
React.useEffect(() => {
if (viewMode === 'editor') {
return;
}
if (!currentSessionId && !newSessionDraftOpen) {
openNewSessionDraft();
}
}, [currentSessionId, newSessionDraftOpen, openNewSessionDraft]);
}, [currentSessionId, newSessionDraftOpen, openNewSessionDraft, viewMode]);
const handleBackToSessions = React.useCallback(() => {
setCurrentView('sessions');
}, []);
// Listen for connection status changes
React.useEffect(() => {
// Catch up with the latest status even if the extension posted the connection message
@@ -147,6 +192,27 @@ export const VSCodeLayout: React.FC = () => {
void runBootstrap();
}, [connectionStatus, configInitialized, hasInitializedOnce, initializeConfig, isInitializing, loadSessions]);
React.useEffect(() => {
if (viewMode !== 'editor') {
return;
}
if (hasAppliedInitialSession.current) {
return;
}
if (!initialSessionId) {
return;
}
if (!hasInitializedOnce || connectionStatus !== 'connected') {
return;
}
if (!sessions.some((session) => session.id === initialSessionId)) {
return;
}
hasAppliedInitialSession.current = true;
void useSessionStore.getState().setCurrentSession(initialSessionId);
}, [connectionStatus, hasInitializedOnce, initialSessionId, sessions, viewMode]);
// Hydrate messages when viewing chat
React.useEffect(() => {
const hydrateMessages = async () => {
@@ -191,10 +257,23 @@ export const VSCodeLayout: React.FC = () => {
return (
<div ref={containerRef} className="h-full w-full bg-background text-foreground flex flex-col">
{currentView === 'sessions' ? (
{viewMode === 'editor' ? (
<div className="flex flex-col h-full">
<VSCodeHeader
title="Sessions"
<VSCodeHeader
title={sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
showMcp
showContextUsage
/>
<div className="flex-1 overflow-hidden">
<ErrorBoundary>
<ChatView />
</ErrorBoundary>
</div>
</div>
) : currentView === 'sessions' ? (
<div className="flex flex-col h-full">
<VSCodeHeader
title="Sessions"
/>
<div className="flex-1 overflow-hidden">
<SessionSidebar
@@ -216,7 +295,7 @@ export const VSCodeLayout: React.FC = () => {
<VSCodeHeader
title={newSessionDraftOpen && !currentSessionId
? 'New session'
: sessions.find(s => s.id === currentSessionId)?.title || 'Chat'}
: sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
showBack
onBack={handleBackToSessions}
showMcp
+13 -2
View File
@@ -87,6 +87,12 @@
"title": "Open Agent Manager",
"icon": "$(circuit-board)"
},
{
"command": "openchamber.openActiveSessionInEditor",
"category": "OpenChamber",
"title": "Open Active Session in Editor",
"icon": "$(link-external)"
},
{
"command": "openchamber.addToContext",
"category": "OpenChamber",
@@ -141,14 +147,19 @@
"group": "navigation@1"
},
{
"command": "openchamber.openAgentManager",
"command": "openchamber.openActiveSessionInEditor",
"when": "view == openchamber.chatView",
"group": "navigation@2"
},
{
"command": "openchamber.showSettings",
"command": "openchamber.openAgentManager",
"when": "view == openchamber.chatView",
"group": "navigation@3"
},
{
"command": "openchamber.showSettings",
"when": "view == openchamber.chatView",
"group": "navigation@4"
}
],
"openchamber.submenu": [
@@ -44,6 +44,11 @@ export class AgentManagerPanelProvider {
}
);
this._panel.iconPath = {
light: vscode.Uri.joinPath(this._extensionUri, 'assets', 'icon.svg'),
dark: vscode.Uri.joinPath(this._extensionUri, 'assets', 'icon-titlebar.svg'),
};
this._panel.webview.html = this._getHtmlForWebview(this._panel.webview);
// Send theme payload (including optional Shiki theme JSON) after the webview is set up.
@@ -0,0 +1,531 @@
import * as vscode from 'vscode';
import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge';
import { getThemeKindName } from './theme';
import type { OpenCodeManager, ConnectionStatus } from './opencode';
import { getWebviewShikiThemes } from './shikiThemes';
import { getWebviewHtml } from './webviewHtml';
type SessionPanelState = {
panel: vscode.WebviewPanel;
sseStreams: Map<string, AbortController>;
sseHeartbeats: Map<string, ReturnType<typeof setInterval>>;
};
export class SessionEditorPanelProvider {
public static readonly viewType = 'openchamber.sessionEditor';
private _cachedStatus: ConnectionStatus = 'connecting';
private _cachedError?: string;
private _sseCounter = 0;
private _panels = new Map<string, SessionPanelState>();
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _extensionUri: vscode.Uri,
private readonly _openCodeManager?: OpenCodeManager
) {}
public createOrShow(sessionId: string, title?: string): void {
if (!sessionId || typeof sessionId !== 'string') {
return;
}
const sessionTitle = title && title.trim().length > 0 ? title.trim() : 'Session';
const existing = this._panels.get(sessionId);
if (existing) {
existing.panel.title = sessionTitle;
existing.panel.reveal(existing.panel.viewColumn ?? vscode.ViewColumn.Active);
return;
}
const distUri = vscode.Uri.joinPath(this._extensionUri, 'dist');
const panel = vscode.window.createWebviewPanel(
SessionEditorPanelProvider.viewType,
sessionTitle,
vscode.ViewColumn.Beside,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [this._extensionUri, distUri],
}
);
panel.iconPath = {
light: vscode.Uri.joinPath(this._extensionUri, 'assets', 'icon.svg'),
dark: vscode.Uri.joinPath(this._extensionUri, 'assets', 'icon-titlebar.svg'),
};
const state: SessionPanelState = {
panel,
sseStreams: new Map(),
sseHeartbeats: new Map(),
};
this._panels.set(sessionId, state);
panel.webview.html = this._getHtmlForWebview(panel.webview, sessionId);
void this.updateTheme(vscode.window.activeColorTheme.kind);
this._sendCachedStateToPanel(state);
panel.onDidDispose(() => {
this._disposePanel(sessionId);
}, null, this._context.subscriptions);
panel.webview.onDidReceiveMessage(async (message: BridgeRequest) => {
if (message.type === 'restartApi') {
await this._openCodeManager?.restart();
return;
}
if (message.type === 'api:sse:start') {
const response = await this._startSseProxy(message, state);
state.panel.webview.postMessage(response);
return;
}
if (message.type === 'api:sse:stop') {
const response = await this._stopSseProxy(message, state);
state.panel.webview.postMessage(response);
return;
}
const response = await handleBridgeMessage(message, {
manager: this._openCodeManager,
context: this._context,
});
state.panel.webview.postMessage(response);
}, null, this._context.subscriptions);
}
public updateTheme(kind: vscode.ColorThemeKind) {
const themeKind = getThemeKindName(kind);
void getWebviewShikiThemes().then((shikiThemes) => {
for (const entry of this._panels.values()) {
entry.panel.webview.postMessage({
type: 'themeChange',
theme: { kind: themeKind, shikiThemes },
});
}
});
}
public updateConnectionStatus(status: ConnectionStatus, error?: string) {
this._cachedStatus = status;
this._cachedError = error;
for (const entry of this._panels.values()) {
this._sendCachedStateToPanel(entry);
}
}
private _sendCachedStateToPanel(entry: SessionPanelState) {
entry.panel.webview.postMessage({
type: 'connectionStatus',
status: this._cachedStatus,
error: this._cachedError,
});
}
private _disposePanel(sessionId: string) {
const entry = this._panels.get(sessionId);
if (!entry) return;
for (const controller of entry.sseStreams.values()) {
controller.abort();
}
entry.sseStreams.clear();
for (const heartbeat of entry.sseHeartbeats.values()) {
clearInterval(heartbeat);
}
entry.sseHeartbeats.clear();
this._panels.delete(sessionId);
}
private _buildSseHeaders(extra?: Record<string, string>): Record<string, string> {
return {
Accept: 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
...(extra || {}),
};
}
private _collectHeaders(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
private async _startSseProxy(message: BridgeRequest, entry: SessionPanelState): Promise<BridgeResponse> {
const { id, type, payload } = message;
const apiBaseUrl = this._openCodeManager?.getApiUrl();
const { path, headers } = (payload || {}) as { path?: string; headers?: Record<string, string> };
const normalizedPath = typeof path === 'string' && path.trim().length > 0 ? path.trim() : '/event';
const shouldInjectActivity = normalizedPath === '/event' || normalizedPath === '/global/event';
if (!apiBaseUrl) {
return {
id,
type,
success: true,
data: { status: 503, headers: { 'content-type': 'application/json' }, streamId: null },
};
}
const streamId = `sse_${++this._sseCounter}_${Date.now()}`;
const controller = new AbortController();
const base = `${apiBaseUrl.replace(/\/+$/, '')}/`;
const targetUrl = new URL(normalizedPath.replace(/^\/+/, ''), base).toString();
let response: Response;
let wrapAsGlobal = false;
const requestHeaders = this._buildSseHeaders(headers || {});
try {
response = await fetch(targetUrl, {
method: 'GET',
headers: requestHeaders,
signal: controller.signal,
});
// Fallback: OpenCode versions without /global/event.
// VS Code is single-workspace, so we can wrap /event into { directory, payload }.
if ((!response.ok || !response.body) && normalizedPath === '/global/event') {
const fallbackUrl = new URL('event', base).toString();
response = await fetch(fallbackUrl, {
method: 'GET',
headers: requestHeaders,
signal: controller.signal,
});
if (response.ok && response.body) {
wrapAsGlobal = true;
}
}
} catch (error) {
const messageText = error instanceof Error ? error.message : String(error);
return {
id,
type,
success: true,
data: { status: 502, headers: { 'content-type': 'application/json' }, streamId: null, error: messageText },
};
}
const responseHeaders = this._collectHeaders(response.headers);
const responseBody = response.body;
if (!response.ok || !responseBody) {
return {
id,
type,
success: true,
data: {
status: response.status,
headers: responseHeaders,
streamId: null,
error: `SSE failed: ${response.status}`,
},
};
}
entry.sseStreams.set(streamId, controller);
const fallbackDirectory = this._openCodeManager?.getWorkingDirectory()
|| vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|| 'global';
if (shouldInjectActivity) {
const heartbeatTimer = setInterval(() => {
if (controller.signal.aborted) {
return;
}
const heartbeatChunk = `${buildHeartbeatEventBlock()}\n\n`;
entry.panel.webview.postMessage({ type: 'api:sse:chunk', streamId, chunk: heartbeatChunk });
}, 30000);
entry.sseHeartbeats.set(streamId, heartbeatTimer);
}
(async () => {
try {
const reader = responseBody.getReader();
const decoder = new TextDecoder();
let sseBuffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (controller.signal.aborted) break;
if (value && value.length > 0) {
const chunk = decoder.decode(value, { stream: true });
if (!chunk) continue;
sseBuffer += shouldInjectActivity ? chunk.replace(/\r\n/g, '\n') : chunk;
const blocks = sseBuffer.split('\n\n');
sseBuffer = blocks.pop() ?? '';
if (blocks.length > 0) {
const routedBlocks = wrapAsGlobal
? wrapSseBlocksAsGlobal(blocks, fallbackDirectory)
: blocks;
const outboundBlocks = shouldInjectActivity ? expandSseBlocksWithActivity(routedBlocks) : routedBlocks;
const joined = outboundBlocks.map((block: string) => `${block}\n\n`).join('');
entry.panel.webview.postMessage({ type: 'api:sse:chunk', streamId, chunk: joined });
}
}
}
const tail = decoder.decode();
if (tail) {
sseBuffer += shouldInjectActivity ? tail.replace(/\r\n/g, '\n') : tail;
}
if (sseBuffer) {
if (shouldInjectActivity) {
const baseBlocks = wrapAsGlobal
? wrapSseBlocksAsGlobal([sseBuffer], fallbackDirectory)
: [sseBuffer];
const outboundBlocks = expandSseBlocksWithActivity(baseBlocks);
const joined = outboundBlocks.map((block: string) => `${block}\n\n`).join('');
entry.panel.webview.postMessage({ type: 'api:sse:chunk', streamId, chunk: joined });
} else {
entry.panel.webview.postMessage({ type: 'api:sse:chunk', streamId, chunk: sseBuffer });
}
}
} finally {
try {
reader.releaseLock();
} catch {
// ignore
}
}
entry.panel.webview.postMessage({ type: 'api:sse:end', streamId });
} catch (error) {
if (!controller.signal.aborted) {
const messageText = error instanceof Error ? error.message : String(error);
entry.panel.webview.postMessage({ type: 'api:sse:end', streamId, error: messageText });
}
} finally {
entry.sseStreams.delete(streamId);
const heartbeat = entry.sseHeartbeats.get(streamId);
if (heartbeat) {
clearInterval(heartbeat);
entry.sseHeartbeats.delete(streamId);
}
}
})();
return {
id,
type,
success: true,
data: {
status: response.status,
headers: responseHeaders,
streamId,
},
};
}
private async _stopSseProxy(message: BridgeRequest, entry: SessionPanelState): Promise<BridgeResponse> {
const { id, type, payload } = message;
const { streamId } = (payload || {}) as { streamId?: string };
if (typeof streamId === 'string' && streamId.length > 0) {
const controller = entry.sseStreams.get(streamId);
if (controller) {
controller.abort();
entry.sseStreams.delete(streamId);
}
const heartbeat = entry.sseHeartbeats.get(streamId);
if (heartbeat) {
clearInterval(heartbeat);
entry.sseHeartbeats.delete(streamId);
}
}
return { id, type, success: true, data: { stopped: true } };
}
private _getHtmlForWebview(webview: vscode.Webview, sessionId: string) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
const initialStatus = this._cachedStatus;
const cliAvailable = this._openCodeManager?.isCliAvailable() ?? false;
return getWebviewHtml({
webview,
extensionUri: this._extensionUri,
workspaceFolder,
initialStatus,
cliAvailable,
panelType: 'chat',
initialSessionId: sessionId,
viewMode: 'editor',
});
}
}
type SessionActivityPhase = 'idle' | 'busy' | 'cooldown';
type SessionActivity = {
sessionId: string;
phase: SessionActivityPhase;
};
const parseSseDataPayload = (block: string): Record<string, unknown> | null => {
if (!block) {
return null;
}
const dataLines = block
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).replace(/^\s/, ''));
if (dataLines.length === 0) {
return null;
}
const payloadText = dataLines.join('\n').trim();
if (!payloadText) {
return null;
}
try {
const parsed = JSON.parse(payloadText) as unknown;
if (parsed && typeof parsed === 'object') {
const record = parsed as Record<string, unknown>;
const nestedPayload = record.payload;
if (nestedPayload && typeof nestedPayload === 'object') {
return nestedPayload as Record<string, unknown>;
}
return record;
}
return null;
} catch {
return null;
}
};
const deriveSessionActivity = (payload: Record<string, unknown> | null): SessionActivity | null => {
if (!payload || typeof payload !== 'object') {
return null;
}
const type = payload.type;
const properties = payload.properties as Record<string, unknown> | undefined;
if (type === 'session.status') {
const status = properties?.status as Record<string, unknown> | undefined;
const sessionId = properties?.sessionID ?? properties?.sessionId;
const statusType = status?.type;
if (typeof sessionId === 'string' && sessionId.length > 0 && typeof statusType === 'string') {
const phase: SessionActivityPhase = statusType === 'busy' || statusType === 'retry' ? 'busy' : 'idle';
return { sessionId, phase };
}
return null;
}
if (type === 'message.updated') {
const info = payload.info as Record<string, unknown> | undefined;
const role = info?.role;
const finish = info?.finish;
const sessionId = info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId;
if (typeof sessionId === 'string' && sessionId.length > 0 && role === 'assistant' && finish === 'stop') {
return { sessionId, phase: 'cooldown' };
}
return null;
}
if (type === 'message.complete') {
const info = payload.info as Record<string, unknown> | undefined;
const role = info?.role;
const finish = info?.finish;
const sessionId = info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId;
if (typeof sessionId === 'string' && sessionId.length > 0 && role === 'assistant' && finish === 'stop') {
return { sessionId, phase: 'cooldown' };
}
return null;
}
if (type === 'session.idle') {
const sessionId = properties?.sessionID ?? properties?.sessionId;
if (typeof sessionId === 'string' && sessionId.length > 0) {
return { sessionId, phase: 'idle' };
}
return null;
}
return null;
};
const buildActivityEventBlock = (activity: SessionActivity): string => {
return [
'event: message',
`data: ${JSON.stringify({
type: 'openchamber:session-activity',
properties: {
sessionId: activity.sessionId,
phase: activity.phase,
at: Date.now(),
},
})}`,
].join('\n');
};
const buildHeartbeatEventBlock = (): string => {
return [
'event: message',
`data: ${JSON.stringify({
type: 'openchamber:heartbeat',
properties: {
at: Date.now(),
},
})}`,
].join('\n');
};
const expandSseBlocksWithActivity = (blocks: string[]): string[] => {
const expanded: string[] = [];
for (const block of blocks) {
expanded.push(block);
const activity = deriveSessionActivity(parseSseDataPayload(block));
if (activity) {
expanded.push(buildActivityEventBlock(activity));
}
}
return expanded;
};
const wrapSseBlocksAsGlobal = (blocks: string[], directory: string): string[] => {
return blocks.map((block) => {
const payload = parseSseDataPayload(block);
const wrappedPayload = payload
? { directory, payload }
: { directory, payload: null };
const updatedLines = block
.split('\n')
.filter((line) => !line.startsWith('data:'));
updatedLines.push(`data: ${JSON.stringify(wrappedPayload)}`);
return updatedLines.join('\n');
});
};
+42
View File
@@ -1,13 +1,18 @@
import * as vscode from 'vscode';
import { ChatViewProvider } from './ChatViewProvider';
import { AgentManagerPanelProvider } from './AgentManagerPanelProvider';
import { SessionEditorPanelProvider } from './SessionEditorPanelProvider';
import { createOpenCodeManager, type OpenCodeManager } from './opencode';
let chatViewProvider: ChatViewProvider | undefined;
let agentManagerProvider: AgentManagerPanelProvider | undefined;
let sessionEditorProvider: SessionEditorPanelProvider | undefined;
let openCodeManager: OpenCodeManager | undefined;
let outputChannel: vscode.OutputChannel | undefined;
let activeSessionId: string | null = null;
let activeSessionTitle: string | null = null;
const SETTINGS_KEY = 'openchamber.settings';
const formatIso = (value: number | null | undefined) => {
@@ -153,6 +158,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Create Agent Manager panel provider
agentManagerProvider = new AgentManagerPanelProvider(context, context.extensionUri, openCodeManager);
sessionEditorProvider = new SessionEditorPanelProvider(context, context.extensionUri, openCodeManager);
context.subscriptions.push(
vscode.commands.registerCommand('openchamber.openAgentManager', () => {
@@ -160,6 +166,38 @@ export async function activate(context: vscode.ExtensionContext) {
})
);
context.subscriptions.push(
vscode.commands.registerCommand('openchamber.setActiveSession', (sessionId: unknown, title?: unknown) => {
if (typeof sessionId === 'string' && sessionId.trim().length > 0) {
activeSessionId = sessionId.trim();
activeSessionTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : null;
return;
}
activeSessionId = null;
activeSessionTitle = null;
})
);
context.subscriptions.push(
vscode.commands.registerCommand('openchamber.openActiveSessionInEditor', () => {
if (!activeSessionId) {
vscode.window.showInformationMessage('OpenChamber: No active session');
return;
}
sessionEditorProvider?.createOrShow(activeSessionId, activeSessionTitle ?? undefined);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('openchamber.openSessionInEditor', (sessionId: string, title?: string) => {
if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
return;
}
sessionEditorProvider?.createOrShow(sessionId.trim(), title);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('openchamber.restartApi', async () => {
try {
@@ -437,6 +475,7 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.window.onDidChangeActiveColorTheme((theme) => {
chatViewProvider?.updateTheme(theme.kind);
agentManagerProvider?.updateTheme(theme.kind);
sessionEditorProvider?.updateTheme(theme.kind);
})
);
@@ -452,6 +491,7 @@ export async function activate(context: vscode.ExtensionContext) {
) {
chatViewProvider?.updateTheme(vscode.window.activeColorTheme.kind);
agentManagerProvider?.updateTheme(vscode.window.activeColorTheme.kind);
sessionEditorProvider?.updateTheme(vscode.window.activeColorTheme.kind);
}
})
);
@@ -461,6 +501,7 @@ export async function activate(context: vscode.ExtensionContext) {
openCodeManager.onStatusChange((status, error) => {
chatViewProvider?.updateConnectionStatus(status, error);
agentManagerProvider?.updateConnectionStatus(status, error);
sessionEditorProvider?.updateConnectionStatus(status, error);
})
);
@@ -474,6 +515,7 @@ export async function deactivate() {
openCodeManager = undefined;
chatViewProvider = undefined;
agentManagerProvider = undefined;
sessionEditorProvider = undefined;
outputChannel?.dispose();
outputChannel = undefined;
}
+15 -2
View File
@@ -11,10 +11,21 @@ export interface WebviewHtmlOptions {
initialStatus: ConnectionStatus;
cliAvailable: boolean;
panelType?: PanelType;
initialSessionId?: string;
viewMode?: 'sidebar' | 'editor';
}
export function getWebviewHtml(options: WebviewHtmlOptions): string {
const { webview, extensionUri, workspaceFolder, initialStatus, cliAvailable, panelType = 'chat' } = options;
const {
webview,
extensionUri,
workspaceFolder,
initialStatus,
cliAvailable,
panelType = 'chat',
initialSessionId,
viewMode = 'sidebar',
} = options;
const scriptPath = vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'assets', 'index.js');
const scriptUri = webview.asWebviewUri(scriptPath);
@@ -131,7 +142,9 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
theme: "${themeKind}",
connectionStatus: "${initialStatus}",
cliAvailable: ${cliAvailable},
panelType: "${panelType}"
panelType: "${panelType}",
viewMode: "${viewMode}",
initialSessionId: ${initialSessionId ? `"${initialSessionId.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : 'null'},
};
window.__OPENCHAMBER_HOME__ = "${workspaceFolder.replace(/\\/g, '\\\\')}";
+2
View File
@@ -23,6 +23,8 @@ declare global {
connectionStatus: string;
cliAvailable?: boolean;
panelType?: PanelType;
viewMode?: 'sidebar' | 'editor';
initialSessionId?: string | null;
};
__OPENCHAMBER_VSCODE_THEME__?: VSCodeThemePayload['theme'];
__OPENCHAMBER_VSCODE_SHIKI_THEMES__?: { light?: Record<string, unknown>; dark?: Record<string, unknown> } | null;