const vscode = require('vscode'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const { chooseLaunchWorkspace, describeProviderState, findCommandPath, isPathInsideWorkspace, parseProfileFile, resolveCommandCheckPath, } = require('./state'); const { buildControlCenterViewModel } = require('./presentation'); const OPENCLAUDE_REPO_URL = 'https://github.com/Gitlawb/openclaude'; const OPENCLAUDE_SETUP_URL = 'https://github.com/Gitlawb/openclaude/blob/main/README.md#quick-start'; const PROFILE_FILE_NAME = '.openclaude-profile.json'; function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } async function isCommandAvailable(command, launchCwd) { return Boolean(findCommandPath(command, { cwd: launchCwd })); } function getExecutableFromCommand(command) { const normalized = String(command || '').trim(); if (!normalized) { return ''; } const doubleQuotedMatch = normalized.match(/^"([^"]+)"/); if (doubleQuotedMatch) { return doubleQuotedMatch[1]; } const singleQuotedMatch = normalized.match(/^'([^']+)'/); if (singleQuotedMatch) { return singleQuotedMatch[1]; } return normalized.split(/\s+/)[0]; } function getWorkspacePaths() { return (vscode.workspace.workspaceFolders || []).map(folder => folder.uri.fsPath); } function getActiveWorkspacePath() { const editor = vscode.window.activeTextEditor; if (!editor || editor.document.uri.scheme !== 'file') { return null; } const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri); return workspaceFolder ? workspaceFolder.uri.fsPath : null; } function getActiveFilePath() { const editor = vscode.window.activeTextEditor; if (!editor || editor.document.uri.scheme !== 'file') { return null; } return editor.document.uri.fsPath || null; } function resolveLaunchTargets({ activeFilePath, workspacePath, workspaceSourceLabel, executable } = {}) { const activeFileDirectory = isPathInsideWorkspace(activeFilePath, workspacePath) ? path.dirname(activeFilePath) : null; const normalizedExecutable = String(executable || '').trim(); const commandPath = normalizedExecutable ? resolveCommandCheckPath(normalizedExecutable, workspacePath) : null; const relativeCommandRequiresWorkspaceRoot = Boolean( workspacePath && commandPath && !path.isAbsolute(normalizedExecutable), ); if (relativeCommandRequiresWorkspaceRoot) { return { projectAwareCwd: workspacePath, projectAwareCwdLabel: workspacePath, projectAwareSourceLabel: 'workspace root (required by relative launch command)', workspaceRootCwd: workspacePath, workspaceRootCwdLabel: workspacePath, launchActionsShareTarget: true, launchActionsShareTargetReason: 'relative-launch-command', }; } if (activeFileDirectory) { return { projectAwareCwd: activeFileDirectory, projectAwareCwdLabel: activeFileDirectory, projectAwareSourceLabel: 'active file directory', workspaceRootCwd: workspacePath || null, workspaceRootCwdLabel: workspacePath || 'No workspace open', launchActionsShareTarget: false, launchActionsShareTargetReason: null, }; } if (workspacePath) { return { projectAwareCwd: workspacePath, projectAwareCwdLabel: workspacePath, projectAwareSourceLabel: workspaceSourceLabel || 'workspace root', workspaceRootCwd: workspacePath, workspaceRootCwdLabel: workspacePath, launchActionsShareTarget: true, launchActionsShareTargetReason: null, }; } return { projectAwareCwd: null, projectAwareCwdLabel: 'VS Code default terminal cwd', projectAwareSourceLabel: 'VS Code default terminal cwd', workspaceRootCwd: null, workspaceRootCwdLabel: 'No workspace open', launchActionsShareTarget: false, launchActionsShareTargetReason: null, }; } function resolveLaunchWorkspace() { return chooseLaunchWorkspace({ activeWorkspacePath: getActiveWorkspacePath(), workspacePaths: getWorkspacePaths(), }); } function getWorkspaceSourceLabel(source) { switch (source) { case 'active-workspace': return 'active editor workspace'; case 'first-workspace': return 'first workspace folder'; default: return 'no workspace open'; } } function getProviderSourceLabel(source) { switch (source) { case 'profile': return 'saved profile'; case 'env': return 'environment'; case 'shim': return 'launch setting'; default: return 'unknown'; } } function readWorkspaceProfile(profilePath) { if (!profilePath || !fs.existsSync(profilePath)) { return { profile: null, statusLabel: 'Missing', statusHint: `${PROFILE_FILE_NAME} not found in the workspace root`, filePath: null, }; } try { const raw = fs.readFileSync(profilePath, 'utf8'); const profile = parseProfileFile(raw); if (!profile) { return { profile: null, statusLabel: 'Invalid', statusHint: `${profilePath} has invalid JSON or an unsupported profile`, filePath: profilePath, }; } return { profile, statusLabel: 'Found', statusHint: profilePath, filePath: profilePath, }; } catch (error) { return { profile: null, statusLabel: 'Unreadable', statusHint: `${profilePath} (${error instanceof Error ? error.message : 'read failed'})`, filePath: profilePath, }; } } async function collectControlCenterState() { const configured = vscode.workspace.getConfiguration('openclaude'); const launchCommand = configured.get('launchCommand', 'openclaude'); const terminalName = configured.get('terminalName', 'OpenClaude'); const shimEnabled = configured.get('useOpenAIShim', false); const executable = getExecutableFromCommand(launchCommand); const launchWorkspace = resolveLaunchWorkspace(); const workspaceFolder = launchWorkspace.workspacePath; const workspaceSourceLabel = getWorkspaceSourceLabel(launchWorkspace.source); const launchTargets = resolveLaunchTargets({ activeFilePath: getActiveFilePath(), workspacePath: workspaceFolder, workspaceSourceLabel, executable, }); const installed = await isCommandAvailable(executable, launchTargets.projectAwareCwd); const profilePath = workspaceFolder ? path.join(workspaceFolder, PROFILE_FILE_NAME) : null; const profileState = workspaceFolder ? readWorkspaceProfile(profilePath) : { profile: null, statusLabel: 'No workspace', statusHint: 'Open a workspace folder to detect a saved profile', filePath: null, }; const providerState = describeProviderState({ shimEnabled, env: process.env, profile: profileState.profile, }); return { installed, executable, launchCommand, terminalName, shimEnabled, workspaceFolder, workspaceSourceLabel, launchCwd: launchTargets.projectAwareCwd, launchCwdLabel: launchTargets.projectAwareCwdLabel, launchCwdSourceLabel: launchTargets.projectAwareSourceLabel, workspaceRootCwd: launchTargets.workspaceRootCwd, workspaceRootCwdLabel: launchTargets.workspaceRootCwdLabel, launchActionsShareTarget: launchTargets.launchActionsShareTarget, launchActionsShareTargetReason: launchTargets.launchActionsShareTargetReason, canLaunchInWorkspaceRoot: Boolean(workspaceFolder), profileStatusLabel: profileState.statusLabel, profileStatusHint: profileState.statusHint, workspaceProfilePath: profileState.filePath, providerState, providerSourceLabel: getProviderSourceLabel(providerState.source), }; } async function launchOpenClaude(options = {}) { const { requireWorkspace = false } = options; const configured = vscode.workspace.getConfiguration('openclaude'); const launchCommand = configured.get('launchCommand', 'openclaude'); const terminalName = configured.get('terminalName', 'OpenClaude'); const shimEnabled = configured.get('useOpenAIShim', false); const executable = getExecutableFromCommand(launchCommand); const launchWorkspace = resolveLaunchWorkspace(); if (requireWorkspace && !launchWorkspace.workspacePath) { await vscode.window.showWarningMessage( 'Open a workspace folder before using Launch in Workspace Root.', ); return; } const launchTargets = resolveLaunchTargets({ activeFilePath: getActiveFilePath(), workspacePath: launchWorkspace.workspacePath, workspaceSourceLabel: getWorkspaceSourceLabel(launchWorkspace.source), executable, }); const targetCwd = requireWorkspace ? launchTargets.workspaceRootCwd : launchTargets.projectAwareCwd; const installed = await isCommandAvailable(executable, targetCwd); if (!installed) { const action = await vscode.window.showErrorMessage( `OpenClaude command not found: ${executable}. Install it with: npm install -g @gitlawb/openclaude`, 'Open Setup Guide', 'Open Repository', ); if (action === 'Open Setup Guide') { await vscode.env.openExternal(vscode.Uri.parse(OPENCLAUDE_SETUP_URL)); } else if (action === 'Open Repository') { await vscode.env.openExternal(vscode.Uri.parse(OPENCLAUDE_REPO_URL)); } return; } const env = {}; if (shimEnabled) { env.CLAUDE_CODE_USE_OPENAI = '1'; } const terminalOptions = { name: terminalName, env, }; if (targetCwd) { terminalOptions.cwd = targetCwd; } const terminal = vscode.window.createTerminal(terminalOptions); terminal.show(true); terminal.sendText(launchCommand, true); } async function openWorkspaceProfile() { const state = await collectControlCenterState(); if (!state.workspaceProfilePath) { await vscode.window.showInformationMessage( `No ${PROFILE_FILE_NAME} file was found for the current workspace.`, ); return; } const document = await vscode.workspace.openTextDocument( vscode.Uri.file(state.workspaceProfilePath), ); await vscode.window.showTextDocument(document, { preview: false }); } function getToneClass(tone) { switch (tone) { case 'accent': return 'tone-accent'; case 'positive': return 'tone-positive'; case 'warning': return 'tone-warning'; case 'critical': return 'tone-critical'; default: return 'tone-neutral'; } } function renderHeaderBadge(badge) { return `
${escapeHtml(badge.label)} ${escapeHtml(badge.value)}
`; } function renderSummaryCard(card) { const detail = card.detail || ''; return `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${detail ? `
${escapeHtml(detail)}
` : ''}
`; } function renderDetailRow(row) { return `
${escapeHtml(row.label)}
${escapeHtml(row.summary)}
${row.detail ? `
${escapeHtml(row.detail)}
` : ''}
`; } function renderDetailSection(section) { const sectionId = `section-${String(section.title || 'section').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`; return `

${escapeHtml(section.title)}

${section.rows.map(renderDetailRow).join('')}
`; } function renderActionButton(action, variant = 'secondary') { return ``; } function renderProfileEmptyState(detail) { return `
No workspace profile yet
${escapeHtml(detail)}
`; } function getPrimaryLaunchActionDetail(status) { if (status.launchActionsShareTargetReason === 'relative-launch-command' && status.launchCwd) { return `Project-aware launch is anchored to the workspace root by the relative command · ${status.launchCwdLabel}`; } if (status.launchCwd && status.launchCwdSourceLabel === 'active file directory') { return `Starts beside the active file · ${status.launchCwdLabel}`; } if (status.launchCwd) { return `Project-aware launch. Currently resolves to ${status.launchCwdSourceLabel} · ${status.launchCwdLabel}`; } return 'Project-aware launch. Uses the VS Code default terminal cwd'; } function getWorkspaceRootActionDetail(status, fallbackDetail) { if (!status.canLaunchInWorkspaceRoot) { return fallbackDetail; } if (status.launchActionsShareTargetReason === 'relative-launch-command') { return `Same workspace-root target as Launch OpenClaude because the relative command resolves from the workspace root · ${status.workspaceRootCwdLabel}`; } return `Always starts at the workspace root · ${status.workspaceRootCwdLabel}`; } function getRenderableViewModel(status) { const viewModel = buildControlCenterViewModel(status); const summaryCards = viewModel.summaryCards.map(card => { if (card.key !== 'launchCwd' || card.detail) { return card; } return { ...card, detail: status.launchCwdSourceLabel || '', }; }); return { ...viewModel, summaryCards, actions: { ...viewModel.actions, primary: { ...viewModel.actions.primary, detail: getPrimaryLaunchActionDetail(status), }, launchRoot: { ...viewModel.actions.launchRoot, detail: getWorkspaceRootActionDetail(status, viewModel.actions.launchRoot.detail), }, }, }; } function renderControlCenterHtml(status, options = {}) { const nonce = options.nonce || crypto.randomBytes(16).toString('base64'); const platform = options.platform || process.platform; const viewModel = getRenderableViewModel(status); const profileActionOrEmpty = viewModel.actions.openProfile ? renderActionButton(viewModel.actions.openProfile) : renderProfileEmptyState(status.profileStatusHint || 'Open a workspace folder to detect a saved profile'); return `
${escapeHtml(viewModel.header.eyebrow)}
OpenClaude

${escapeHtml(viewModel.header.title)}

${escapeHtml(viewModel.header.subtitle)}

${viewModel.headerBadges.map(renderHeaderBadge).join('')}
${viewModel.summaryCards.map(renderSummaryCard).join('')}
${viewModel.detailSections.map(renderDetailSection).join('')}

Launch & Project

${renderActionButton(viewModel.actions.primary, 'primary')}
${renderActionButton(viewModel.actions.launchRoot)} ${profileActionOrEmpty}
Settings and workspace status stay in view here. Reference links stay secondary.
`; } class OpenClaudeControlCenterProvider { constructor() { this.webviewView = null; } async resolveWebviewView(webviewView) { this.webviewView = webviewView; webviewView.webview.options = { enableScripts: true }; webviewView.onDidDispose(() => { if (this.webviewView === webviewView) { this.webviewView = null; } }); webviewView.webview.onDidReceiveMessage(async message => { switch (message?.type) { case 'launch': await launchOpenClaude(); break; case 'launchRoot': await launchOpenClaude({ requireWorkspace: true }); break; case 'openProfile': await openWorkspaceProfile(); break; case 'repo': await vscode.env.openExternal(vscode.Uri.parse(OPENCLAUDE_REPO_URL)); break; case 'setup': await vscode.env.openExternal(vscode.Uri.parse(OPENCLAUDE_SETUP_URL)); break; case 'commands': await vscode.commands.executeCommand('workbench.action.showCommands'); break; case 'refresh': default: break; } await this.refresh(); }); await this.refresh(); } async refresh() { if (!this.webviewView) { return; } try { const status = await collectControlCenterState(); this.webviewView.webview.html = this.getHtml(status); } catch (error) { this.webviewView.webview.html = this.getErrorHtml(error); } } getErrorHtml(error) { const nonce = crypto.randomBytes(16).toString('base64'); const message = error instanceof Error ? error.message : 'Unknown Control Center error'; return `
Control Center Error
${escapeHtml(message)}
`; } getHtml(status) { const nonce = crypto.randomBytes(16).toString('base64'); return renderControlCenterHtml(status, { nonce, platform: process.platform }); } } /** * @param {vscode.ExtensionContext} context */ function activate(context) { const provider = new OpenClaudeControlCenterProvider(); const refreshProvider = () => { void provider.refresh(); }; const startCommand = vscode.commands.registerCommand('openclaude.start', async () => { await launchOpenClaude(); }); const startInWorkspaceRootCommand = vscode.commands.registerCommand( 'openclaude.startInWorkspaceRoot', async () => { await launchOpenClaude({ requireWorkspace: true }); }, ); const openDocsCommand = vscode.commands.registerCommand('openclaude.openDocs', async () => { await vscode.env.openExternal(vscode.Uri.parse(OPENCLAUDE_REPO_URL)); }); const openSetupDocsCommand = vscode.commands.registerCommand( 'openclaude.openSetupDocs', async () => { await vscode.env.openExternal(vscode.Uri.parse(OPENCLAUDE_SETUP_URL)); }, ); const openWorkspaceProfileCommand = vscode.commands.registerCommand( 'openclaude.openWorkspaceProfile', async () => { await openWorkspaceProfile(); }, ); const openUiCommand = vscode.commands.registerCommand('openclaude.openControlCenter', async () => { await vscode.commands.executeCommand('workbench.view.extension.openclaude'); }); const providerDisposable = vscode.window.registerWebviewViewProvider( 'openclaude.controlCenter', provider, ); const profileWatcher = vscode.workspace.createFileSystemWatcher(`**/${PROFILE_FILE_NAME}`); context.subscriptions.push( startCommand, startInWorkspaceRootCommand, openDocsCommand, openSetupDocsCommand, openWorkspaceProfileCommand, openUiCommand, providerDisposable, profileWatcher, vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration('openclaude')) { refreshProvider(); } }), vscode.workspace.onDidChangeWorkspaceFolders(refreshProvider), vscode.window.onDidChangeActiveTextEditor(refreshProvider), profileWatcher.onDidCreate(refreshProvider), profileWatcher.onDidChange(refreshProvider), profileWatcher.onDidDelete(refreshProvider), ); } function deactivate() {} module.exports = { activate, deactivate, OpenClaudeControlCenterProvider, renderControlCenterHtml, resolveLaunchTargets, };