* feat: Implement project management store with project path validation and synchronization - Added `useProjectsStore` for managing projects, including adding, removing, renaming, and validating project paths. - Implemented persistence for projects and active project ID using safe storage. - Introduced synchronization from desktop settings to keep project data consistent. - Enhanced session store to manage sessions by directory and added new methods for session management. - Updated todo store to fetch session todos based on the directory context. - Refactored server code to validate and resolve project directories for various API endpoints. - Added project entry validation and sanitization to ensure data integrity. * feat(settings): migrate legacy project settings and update settings loading logic * feat: enhance project management with directory-aware settings and improved agent/command source handling * feat: enhance session and project management with directory-aware settings and improved configuration refresh logic * feat: enhance project management with worktree manager integration and project directory resolution * feat: enhance agent groups store with project directory resolution and loading logic * feat: add heartbeat management and global wrapping for SSE blocks in agent and chat providers * feat: refactor command and project handling in useCommandsStore - Replaced useDirectoryStore with useProjectsStore to manage project paths. - Introduced getRequestDirectory function to determine the active project directory. - Updated command fetching to respect project-level scoping. - Enhanced error handling and logging for command configuration fetching. - Improved command configuration saving and updating to utilize project directory context. feat: enhance project path normalization in useProjectsStore - Added resolveTildePath function to expand paths starting with ~. - Updated normalizeProjectPath to utilize home directory for path expansion. fix: update permission handling in useSessionStore - Changed Permission type to PermissionRequest for clarity. - Updated respondToPermission method to use requestId instead of permissionId. refactor: improve permission utilities - Introduced types for PermissionAction and PermissionRule. - Enhanced getAgentDefinition and resolveConfigStore functions for better type safety. - Added resolvePermissionAction to streamline permission resolution logic. feat: add agent configuration retrieval endpoint - Implemented new API endpoint to fetch agent configuration based on project directory. - Enhanced getAgentPermissionSource to prioritize project-level permissions. chore: update SDK version in package.json files - Bumped @opencode-ai/sdk version to ^1.1.1 across all relevant package.json files. refactor: streamline bridge message handling - Updated handleBridgeMessage to accept directory parameter for agent and command requests. - Improved local API request handling to extract directory from query parameters and headers. feat: enhance project configuration management - Added functions to retrieve and merge project configuration paths. - Improved handling of existing project configuration files for agents and commands. * feat: enhance VSCode integration and session management - Added support for a sticky sidebar header background in light and dark themes. - Introduced functions to read VSCode workspace directory and check if running in VSCode. - Implemented detailed logging for session loading and creation processes. - Enhanced session filtering based on directory structure and canonical paths. - Added a new method to reorder projects and prevent modifications in VSCode workspace. - Improved error handling and logging for app initialization and markdown file parsing. - Updated API checks and health checks to ensure readiness before proceeding. - Refactored code for better readability and maintainability across various modules. * feat: improve agent and branch selection logic, enhance session management, and update multi-run creation response * feat: add worktree management actions in agent group detail and sidebar, including delete and keep only options * fix(ui): share IME guard and cover multi-run * fix(session): reduce maximum visible sessions in group from 7 to 5
478 lines
19 KiB
TypeScript
478 lines
19 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { ChatViewProvider } from './ChatViewProvider';
|
|
import { AgentManagerPanelProvider } from './AgentManagerPanelProvider';
|
|
import { createOpenCodeManager, type OpenCodeManager } from './opencode';
|
|
|
|
let chatViewProvider: ChatViewProvider | undefined;
|
|
let agentManagerProvider: AgentManagerPanelProvider | undefined;
|
|
let openCodeManager: OpenCodeManager | undefined;
|
|
let outputChannel: vscode.OutputChannel | undefined;
|
|
|
|
const SETTINGS_KEY = 'openchamber.settings';
|
|
|
|
const formatIso = (value: number | null | undefined) => {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) return '(none)';
|
|
try {
|
|
return new Date(value).toISOString();
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
};
|
|
|
|
const formatDurationMs = (value: number | null | undefined) => {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) return '(none)';
|
|
const seconds = Math.round(value / 100) / 10;
|
|
return `${seconds}s`;
|
|
};
|
|
|
|
export async function activate(context: vscode.ExtensionContext) {
|
|
outputChannel = vscode.window.createOutputChannel('OpenChamber');
|
|
|
|
let moveToRightSidebarScheduled = false;
|
|
|
|
const isCursorLikeHost = () => /\bcursor\b/i.test(vscode.env.appName);
|
|
|
|
const findMoveToRightSidebarCommandId = async (): Promise<string | null> => {
|
|
const commands = await vscode.commands.getCommands(true);
|
|
|
|
const preferred = [
|
|
// Newer VS Code naming
|
|
'workbench.action.moveViewToSecondarySideBar',
|
|
'workbench.action.moveViewToSecondarySidebar',
|
|
'workbench.action.moveFocusedViewToSecondarySideBar',
|
|
'workbench.action.moveFocusedViewToSecondarySidebar',
|
|
|
|
// Some builds use "Auxiliary Bar" naming
|
|
'workbench.action.moveViewToAuxiliaryBar',
|
|
'workbench.action.moveFocusedViewToAuxiliaryBar',
|
|
];
|
|
|
|
for (const commandId of preferred) {
|
|
if (commands.includes(commandId)) return commandId;
|
|
}
|
|
|
|
const fuzzy = commands.find((commandId) => {
|
|
const id = commandId.toLowerCase();
|
|
const looksLikeMoveView = id.includes('workbench.action') && id.includes('move') && id.includes('view');
|
|
if (!looksLikeMoveView) return false;
|
|
|
|
// Support both "secondary sidebar" and "auxiliary bar" naming.
|
|
return (id.includes('secondary') && id.includes('side') && id.includes('bar')) || (id.includes('auxiliary') && id.includes('bar'));
|
|
});
|
|
|
|
return fuzzy || null;
|
|
};
|
|
|
|
const attemptMoveChatToRightSidebar = async (): Promise<'moved' | 'unsupported' | 'failed'> => {
|
|
const moveCommandId = await findMoveToRightSidebarCommandId();
|
|
if (!moveCommandId) return 'unsupported';
|
|
|
|
try {
|
|
await vscode.commands.executeCommand('openchamber.chatView.focus');
|
|
await vscode.commands.executeCommand(moveCommandId);
|
|
return 'moved';
|
|
} catch (error) {
|
|
outputChannel?.appendLine(
|
|
`[OpenChamber] Failed moving chat view to right sidebar (command=${moveCommandId}): ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
return 'failed';
|
|
}
|
|
};
|
|
|
|
const maybeMoveChatToRightSidebarOnStartup = async () => {
|
|
if (isCursorLikeHost()) return;
|
|
|
|
const attempted = context.globalState.get<boolean>('openchamber.sidebarAutoMoveAttempted') || false;
|
|
if (attempted) return;
|
|
await context.globalState.update('openchamber.sidebarAutoMoveAttempted', true);
|
|
|
|
if (moveToRightSidebarScheduled) return;
|
|
moveToRightSidebarScheduled = true;
|
|
|
|
// Defer until after activation to avoid stealing focus during startup.
|
|
setTimeout(() => {
|
|
void (async () => {
|
|
try {
|
|
await attemptMoveChatToRightSidebar();
|
|
} finally {
|
|
moveToRightSidebarScheduled = false;
|
|
}
|
|
})();
|
|
}, 800);
|
|
};
|
|
|
|
|
|
// Migration: clear legacy auto-set API URLs (ports 47680-47689 were auto-assigned by older extension versions)
|
|
const config = vscode.workspace.getConfiguration('openchamber');
|
|
const legacyApiUrl = config.get<string>('apiUrl') || '';
|
|
if (/^https?:\/\/localhost:4768\d\/?$/.test(legacyApiUrl.trim())) {
|
|
await config.update('apiUrl', '', vscode.ConfigurationTarget.Global);
|
|
}
|
|
|
|
// Create OpenCode manager first
|
|
openCodeManager = createOpenCodeManager(context);
|
|
|
|
// Create chat view provider with manager reference
|
|
// The webview will show a loading state until OpenCode is ready
|
|
chatViewProvider = new ChatViewProvider(context, context.extensionUri, openCodeManager);
|
|
|
|
context.subscriptions.push(
|
|
vscode.window.registerWebviewViewProvider(
|
|
ChatViewProvider.viewType,
|
|
chatViewProvider,
|
|
{ webviewOptions: { retainContextWhenHidden: true } }
|
|
)
|
|
);
|
|
|
|
// Register sidebar/focus commands AFTER the webview view provider is registered
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.openSidebar', async () => {
|
|
// Best-effort: open the container (if available), then focus the chat view.
|
|
try {
|
|
await vscode.commands.executeCommand('workbench.view.extension.openchamber');
|
|
} catch (e) {
|
|
outputChannel?.appendLine(`[OpenChamber] workbench.view.extension.openchamber failed: ${e}`);
|
|
}
|
|
|
|
try {
|
|
await vscode.commands.executeCommand('openchamber.chatView.focus');
|
|
} catch (e) {
|
|
outputChannel?.appendLine(`[OpenChamber] openchamber.chatView.focus failed: ${e}`);
|
|
vscode.window.showErrorMessage(`OpenChamber: Failed to open sidebar - ${e}`);
|
|
}
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.focusChat', async () => {
|
|
await vscode.commands.executeCommand('openchamber.chatView.focus');
|
|
})
|
|
);
|
|
|
|
void maybeMoveChatToRightSidebarOnStartup();
|
|
|
|
// Create Agent Manager panel provider
|
|
agentManagerProvider = new AgentManagerPanelProvider(context, context.extensionUri, openCodeManager);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.openAgentManager', () => {
|
|
agentManagerProvider?.createOrShow();
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.restartApi', async () => {
|
|
try {
|
|
await openCodeManager?.restart();
|
|
vscode.window.showInformationMessage('OpenChamber: API connection restarted');
|
|
} catch (e) {
|
|
vscode.window.showErrorMessage(`OpenChamber: Failed to restart API - ${e}`);
|
|
}
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.addToContext', async () => {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor) {
|
|
vscode.window.showWarningMessage('OpenChamber [Add to Context]:No active editor');
|
|
return;
|
|
}
|
|
|
|
const selection = editor.selection;
|
|
const selectedText = editor.document.getText(selection);
|
|
|
|
if (!selectedText) {
|
|
vscode.window.showWarningMessage('OpenChamber [Add to Context]: No text selected');
|
|
return;
|
|
}
|
|
|
|
// Get file info for context
|
|
const filePath = vscode.workspace.asRelativePath(editor.document.uri);
|
|
const languageId = editor.document.languageId;
|
|
|
|
// Get line numbers (1-based for display)
|
|
const startLine = selection.start.line + 1;
|
|
const endLine = selection.end.line + 1;
|
|
const lineRange = startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
|
|
|
|
// Format as file path with line numbers, followed by markdown code block
|
|
const contextText = `${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
|
|
|
// Send to webview and reveal the panel
|
|
chatViewProvider?.addTextToInput(contextText);
|
|
|
|
// Focus the chat panel
|
|
vscode.commands.executeCommand('openchamber.focusChat');
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.explain', async () => {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor) {
|
|
vscode.window.showWarningMessage('OpenChamber [Explain]: No active editor');
|
|
return;
|
|
}
|
|
|
|
const selection = editor.selection;
|
|
const selectedText = editor.document.getText(selection);
|
|
const filePath = vscode.workspace.asRelativePath(editor.document.uri);
|
|
const languageId = editor.document.languageId;
|
|
|
|
let prompt: string;
|
|
|
|
if (selectedText) {
|
|
// Selection exists - explain the selected code
|
|
const startLine = selection.start.line + 1;
|
|
const endLine = selection.end.line + 1;
|
|
const lineRange = startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
|
|
prompt = `Explain the following Code / Text:\n\n${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
|
} else {
|
|
// No selection - explain the entire file
|
|
prompt = `Explain the following Code / Text:\n\n${filePath}`;
|
|
}
|
|
|
|
// Create new session and send the prompt
|
|
chatViewProvider?.createNewSessionWithPrompt(prompt);
|
|
vscode.commands.executeCommand('openchamber.focusChat');
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.improveCode', async () => {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor) {
|
|
vscode.window.showWarningMessage('OpenChamber [Improve Code]: No active editor');
|
|
return;
|
|
}
|
|
|
|
const selection = editor.selection;
|
|
const selectedText = editor.document.getText(selection);
|
|
|
|
if (!selectedText) {
|
|
vscode.window.showWarningMessage('OpenChamber [Improve Code]: No text selected');
|
|
return;
|
|
}
|
|
|
|
const filePath = vscode.workspace.asRelativePath(editor.document.uri);
|
|
const languageId = editor.document.languageId;
|
|
const startLine = selection.start.line + 1;
|
|
const endLine = selection.end.line + 1;
|
|
const lineRange = startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
|
|
|
|
const prompt = `Improve the following Code:\n\n${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
|
|
|
// Create new session and send the prompt
|
|
chatViewProvider?.createNewSessionWithPrompt(prompt);
|
|
vscode.commands.executeCommand('openchamber.focusChat');
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.newSession', () => {
|
|
chatViewProvider?.createNewSession();
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.showSettings', () => {
|
|
chatViewProvider?.showSettings();
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('openchamber.showOpenCodeStatus', async () => {
|
|
const config = vscode.workspace.getConfiguration('openchamber');
|
|
const configuredApiUrl = (config.get<string>('apiUrl') || '').trim();
|
|
|
|
const extensionVersion = String(context.extension?.packageJSON?.version || '');
|
|
const workspaceFolders = (vscode.workspace.workspaceFolders || []).map((folder) => folder.uri.fsPath);
|
|
|
|
const debug = openCodeManager?.getDebugInfo();
|
|
const resolvedApiUrl = openCodeManager?.getApiUrl();
|
|
const workingDirectory = openCodeManager?.getWorkingDirectory() ?? '';
|
|
|
|
const safeFetch = async (input: string, timeoutMs = 2500) => {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
const startedAt = Date.now();
|
|
try {
|
|
const resp = await fetch(input, {
|
|
method: 'GET',
|
|
headers: { Accept: 'application/json' },
|
|
signal: controller.signal,
|
|
});
|
|
const elapsedMs = Date.now() - startedAt;
|
|
const contentType = resp.headers.get('content-type') || '';
|
|
const isJson = contentType.toLowerCase().includes('json') && !contentType.toLowerCase().includes('text/html');
|
|
|
|
let summary = '';
|
|
if (isJson) {
|
|
const json = await resp.json().catch(() => null);
|
|
if (Array.isArray(json)) {
|
|
summary = `json[array] len=${json.length}`;
|
|
} else if (json && typeof json === 'object') {
|
|
const keys = Object.keys(json).slice(0, 8);
|
|
summary = `json[object] keys=${keys.join(',')}${Object.keys(json).length > keys.length ? ',…' : ''}`;
|
|
} else {
|
|
summary = `json[${typeof json}]`;
|
|
}
|
|
} else {
|
|
summary = contentType ? `content-type=${contentType}` : 'no content-type';
|
|
}
|
|
|
|
return { ok: resp.ok && isJson, status: resp.status, elapsedMs, summary };
|
|
} catch (error) {
|
|
const elapsedMs = Date.now() - startedAt;
|
|
const isAbort =
|
|
controller.signal.aborted ||
|
|
(error instanceof Error && (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted')));
|
|
const message = isAbort
|
|
? `timeout after ${timeoutMs}ms`
|
|
: error instanceof Error
|
|
? error.message
|
|
: String(error);
|
|
return { ok: false, status: 0, elapsedMs, summary: `error=${message}` };
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
};
|
|
|
|
const buildProbeUrl = (pathname: string, includeDirectory = true) => {
|
|
if (!resolvedApiUrl) return null;
|
|
const base = `${resolvedApiUrl.replace(/\/+$/, '')}/`;
|
|
const url = new URL(pathname.replace(/^\/+/, ''), base);
|
|
if (includeDirectory && workingDirectory) {
|
|
url.searchParams.set('directory', workingDirectory);
|
|
}
|
|
return url.toString();
|
|
};
|
|
|
|
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
|
|
{ label: 'config', path: '/config', includeDirectory: true },
|
|
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
|
// Can be slower on large configs; keep the probe from producing false negatives.
|
|
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 8000 },
|
|
{ label: 'commands', path: '/command', includeDirectory: true },
|
|
{ label: 'project', path: '/project/current', includeDirectory: true },
|
|
{ label: 'path', path: '/path', includeDirectory: true },
|
|
// Session listing is what powers the sidebar. This helps diagnose "no sessions shown" bugs.
|
|
{ label: 'sessions', path: '/session', includeDirectory: true, timeoutMs: 8000 },
|
|
{ label: 'sessionStatus', path: '/session/status', includeDirectory: true },
|
|
];
|
|
|
|
const probes = resolvedApiUrl
|
|
? await Promise.all(
|
|
probeTargets.map(async (entry) => {
|
|
const url = buildProbeUrl(entry.path, entry.includeDirectory !== false);
|
|
if (!url) {
|
|
return { label: entry.label, url: '(none)', result: null as null };
|
|
}
|
|
const result = await safeFetch(url, typeof entry.timeoutMs === 'number' ? entry.timeoutMs : undefined);
|
|
return { label: entry.label, url, result };
|
|
})
|
|
)
|
|
: [];
|
|
|
|
const storedSettings = context.globalState.get<Record<string, unknown>>(SETTINGS_KEY) || {};
|
|
const settingsKeys = Object.keys(storedSettings).filter((key) => key !== 'lastDirectory');
|
|
|
|
const lines = [
|
|
`Time: ${new Date().toISOString()}`,
|
|
`OpenChamber version: ${extensionVersion || '(unknown)'}`,
|
|
`VS Code version: ${vscode.version}`,
|
|
`Platform: ${process.platform} ${process.arch}`,
|
|
`Workspace folders: ${workspaceFolders.length}${workspaceFolders.length ? ` (${workspaceFolders.join(', ')})` : ''}`,
|
|
`Status: ${openCodeManager?.getStatus() ?? 'unknown'}`,
|
|
`CLI available: ${openCodeManager?.isCliAvailable() ?? false}`,
|
|
`Working directory: ${openCodeManager?.getWorkingDirectory() ?? ''}`,
|
|
`API URL (configured): ${configuredApiUrl || '(none)'}`,
|
|
`API URL (resolved): ${openCodeManager?.getApiUrl() ?? '(none)'}`,
|
|
debug
|
|
? `OpenCode mode: ${debug.mode} (starts=${debug.startCount}, restarts=${debug.restartCount})`
|
|
: `OpenCode mode: (unknown)`,
|
|
debug
|
|
? `OpenCode CLI path: ${debug.cliPath || '(not found)'}`
|
|
: `OpenCode CLI path: (unknown)`,
|
|
debug
|
|
? `OpenCode detected port: ${debug.detectedPort ?? '(none)'}`
|
|
: `OpenCode detected port: (unknown)`,
|
|
debug
|
|
? `OpenCode API prefix: ${debug.apiPrefixDetected ? (debug.apiPrefix || '(root)') : '(unknown)'}`
|
|
: `OpenCode API prefix: (unknown)`,
|
|
debug
|
|
? `Last start: ${formatIso(debug.lastStartAt)}`
|
|
: `Last start: (unknown)`,
|
|
debug
|
|
? `Last connected: ${formatIso(debug.lastConnectedAt)}`
|
|
: `Last connected: (unknown)`,
|
|
debug && debug.lastConnectedAt ? `Connected for: ${formatDurationMs(Date.now() - debug.lastConnectedAt)}` : `Connected for: (n/a)`,
|
|
debug && debug.lastExitCode !== null ? `Last exit code: ${debug.lastExitCode}` : `Last exit code: (none)`,
|
|
debug?.lastError ? `Last error: ${debug.lastError}` : `Last error: (none)`,
|
|
`Settings keys (stored): ${settingsKeys.length ? settingsKeys.join(', ') : '(none)'}`,
|
|
probes.length ? '' : '',
|
|
...(probes.length
|
|
? [
|
|
'OpenCode API probes:',
|
|
...probes.map((probe) => {
|
|
if (!probe.result) return `- ${probe.label}: (no url)`;
|
|
const { ok, status, elapsedMs, summary } = probe.result;
|
|
const suffix = ok ? '' : ` url=${probe.url}`;
|
|
return `- ${probe.label}: ${ok ? 'ok' : 'fail'} status=${status} time=${elapsedMs}ms ${summary}${suffix}`;
|
|
}),
|
|
]
|
|
: []),
|
|
'',
|
|
];
|
|
|
|
outputChannel?.appendLine(lines.join('\n'));
|
|
outputChannel?.show(true);
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.window.onDidChangeActiveColorTheme((theme) => {
|
|
chatViewProvider?.updateTheme(theme.kind);
|
|
agentManagerProvider?.updateTheme(theme.kind);
|
|
})
|
|
);
|
|
|
|
// Theme changes can update the `workbench.colorTheme` setting slightly after the
|
|
// `activeColorTheme` event. Listen for config changes too so we can re-resolve
|
|
// the contributed theme JSON and update Shiki themes in the webview.
|
|
context.subscriptions.push(
|
|
vscode.workspace.onDidChangeConfiguration((event) => {
|
|
if (
|
|
event.affectsConfiguration('workbench.colorTheme') ||
|
|
event.affectsConfiguration('workbench.preferredLightColorTheme') ||
|
|
event.affectsConfiguration('workbench.preferredDarkColorTheme')
|
|
) {
|
|
chatViewProvider?.updateTheme(vscode.window.activeColorTheme.kind);
|
|
agentManagerProvider?.updateTheme(vscode.window.activeColorTheme.kind);
|
|
}
|
|
})
|
|
);
|
|
|
|
// Subscribe to status changes - this broadcasts to webview
|
|
context.subscriptions.push(
|
|
openCodeManager.onStatusChange((status, error) => {
|
|
chatViewProvider?.updateConnectionStatus(status, error);
|
|
agentManagerProvider?.updateConnectionStatus(status, error);
|
|
})
|
|
);
|
|
|
|
// Start OpenCode API without blocking activation.
|
|
// Blocking here delays webview resolution and causes a blank panel until startup completes.
|
|
void openCodeManager.start();
|
|
}
|
|
|
|
export async function deactivate() {
|
|
await openCodeManager?.stop();
|
|
openCodeManager = undefined;
|
|
chatViewProvider = undefined;
|
|
agentManagerProvider = undefined;
|
|
outputChannel?.dispose();
|
|
outputChannel = undefined;
|
|
}
|