import * as vscode from 'vscode'; import * as path from 'path'; import { SidebarChatProvider } from '../sidebarProvider'; import { getActiveBrainProfile, logInfo } from '../utils'; import { pickConfigTarget } from '../lib/paths'; /** * Handles chat-domain messages: prompts, model selection, sessions, streaming control, * generic webview transport (export, settings, addMessage), action approvals, and the * cross-cutting `ready` bootstrap. * * Returns true when the message was handled by this domain, false otherwise — the * caller chains domain handlers until one accepts the message. */ export async function handleChatMessage(provider: SidebarChatProvider, data: any): Promise { switch (data.type) { case 'prompt': case 'promptWithFile': provider._lmStudio?.activity.bump(); await provider._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); // ── 1인 기업 모드 우선 분기 ── // When company mode is active, route the prompt through the // CEO planner / sequential dispatcher / synthesis pipeline // instead of the normal single-agent path. The user-facing // chat surface is the same — only the runtime differs. if (provider.isCompanyModeEnabled() && typeof data.value === 'string' && data.value.trim()) { await provider._runCompanyTurn(data.value.trim()); return true; } await provider._handlePrompt(data); await provider._autoWriteChronicleAfterPrompt(); await provider._saveCurrentSession(); return true; case 'activity': provider._lmStudio?.activity.bump(); return true; case 'ready': await provider._sendBrainStatus(); await provider._sendBrainProfiles(); await provider._sendSessionList(); await provider._sendModels(); await provider._sendChronicleProjects(); await provider._restoreActiveSessionIntoView(); await provider._sendReadyStatus(); // Restore the Project Architecture chip + watcher if the active project // was already running in architecture mode in a previous VS Code session. await provider._sendArchitectureStatus(); // Restore the Company chip from globalState so the user sees the same // mode they had on at last shutdown. await provider._sendCompanyStatus(); return true; case 'getReadyStatus': await provider._sendReadyStatus(); return true; case 'createLessonFromConversation': await vscode.commands.executeCommand('g1nation.lesson.fromConversation'); return true; case 'manageLessons': await vscode.commands.executeCommand('g1nation.lesson.manage'); return true; case 'getModels': await provider._sendModels(); return true; case 'getSessions': await provider._sendSessionList(); return true; case 'newChat': provider._currentSessionId = null; provider._currentSessionBrainId = getActiveBrainProfile().id; provider._agent.resetConversation(); await provider._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null); await provider._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null); await provider._context.globalState.update(SidebarChatProvider.blankChatStateKey, true); provider.clearChat(); await provider._sendBrainStatus(); return true; case 'stopGeneration': provider._agent.stop(); return true; case 'loadSession': await provider._loadSession(data.id); return true; case 'deleteSession': await provider._deleteSession(data.id); return true; case 'openSettings': // Route the sidebar gear button to Astra's own settings webview. // Falls back to VS Code Settings if the view hasn't registered yet // (e.g. during the very first activation pass) and surfaces any // unexpected error so the user isn't stuck with a silent button. try { await vscode.commands.executeCommand('g1nation.settings.focus'); } catch (e: any) { logInfo('openSettings: settings.focus failed, falling back to VS Code Settings.', { error: e?.message ?? String(e) }); try { await vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation'); } catch (e2: any) { vscode.window.showErrorMessage(`Astra Settings 열기 실패: ${e2?.message ?? e2}`); } } return true; case 'addMessage': provider._view?.webview.postMessage({ type: 'addMessage', role: data.role, value: data.value, rationale: data.rationale }); return true; case 'refreshModels': await provider._sendModels(true); return true; case 'model': { // Write to whichever scope already holds the value so a stale // Workspace override doesn't shadow our Global update — that was // the "sidebar shows e2b but Settings shows e4b" desync. const { target } = pickConfigTarget('g1nation', 'defaultModel'); await vscode.workspace.getConfiguration('g1nation').update('defaultModel', data.value, target); logInfo(`Default model updated to: ${data.value}`, { target }); provider._lmStudio?.lifecycle.onModelSelected(data.value); return true; } case 'getKnowledgeMix': { // Ship the current global Knowledge Mix to the webview so the slider can // initialize. Per-agent overrides ride along with the agent map data. const cfg = vscode.workspace.getConfiguration('g1nation'); const w = cfg.get('knowledgeMix.secondBrainWeight', 50); const clamped = Math.max(0, Math.min(100, Math.round(typeof w === 'number' ? w : 50))); provider._view?.webview.postMessage({ type: 'knowledgeMix', value: { weight: clamped, source: 'global' }, }); return true; } case 'setKnowledgeMix': { const raw = typeof data.value === 'number' ? data.value : NaN; if (!Number.isFinite(raw)) return true; const clamped = Math.max(0, Math.min(100, Math.round(raw))); // Use whichever scope already holds the value to avoid the same "Workspace // override shadows Global update" desync that the `model` case guards against. const { target } = pickConfigTarget('g1nation', 'knowledgeMix.secondBrainWeight'); await vscode.workspace.getConfiguration('g1nation').update('knowledgeMix.secondBrainWeight', clamped, target); logInfo(`Knowledge Mix (global) updated to: ${clamped}`, { target }); return true; } // ── Project Architecture (Feature 2) ────────────────────────────────── case 'getArchitectureStatus': await provider._sendArchitectureStatus(); return true; case 'openArchitectureDoc': await provider._openArchitectureDoc(); return true; case 'refreshArchitecture': await provider._refreshArchitecture(); return true; case 'detachArchitecture': await provider._detachArchitecture(); return true; case 'activateArchitectureFromText': { // Optional explicit-toggle path: webview can pass arbitrary text // (e.g. the current input draft) for one-shot intent detection. if (typeof data.text === 'string') { await provider._tryActivateArchitectureFromText(data.text); } return true; } // ── 1인 기업 모드 메시지 라우팅 ──────────────────────────────────── case 'getCompanyStatus': await provider._sendCompanyStatus(); return true; case 'getCompanyAgents': await provider._sendCompanyAgents(); return true; case 'setCompanyEnabled': { const { setCompanyEnabled } = await import('../features/company'); await setCompanyEnabled(provider._context, !!data.value); await provider._sendCompanyStatus(); return true; } case 'setCompanyName': { const { setCompanyName } = await import('../features/company'); await setCompanyName(provider._context, typeof data.value === 'string' ? data.value : ''); await provider._sendCompanyStatus(); return true; } case 'setCompanyActiveAgents': { const { setActiveAgents } = await import('../features/company'); const ids = Array.isArray(data.value) ? data.value.filter((v: unknown): v is string => typeof v === 'string') : []; await setActiveAgents(provider._context, ids); await provider._sendCompanyStatus(); await provider._sendCompanyAgents(); return true; } case 'setCompanyAgentModel': { const { setAgentModelOverride } = await import('../features/company'); const agentId = typeof data.agentId === 'string' ? data.agentId : ''; const model = typeof data.model === 'string' ? data.model : ''; if (agentId) { await setAgentModelOverride(provider._context, agentId, model); await provider._sendCompanyAgents(); } return true; } case 'setCompanyAgentPrompt': { // Patch one agent's persona / specialty / tagline. Each field is // optional in the payload; passing an *empty string* explicitly // clears that field (back to the default from `agents.ts`). // Sending `null` for the whole override resets every field at once. const { setAgentPromptOverride } = await import('../features/company'); const agentId = typeof data.agentId === 'string' ? data.agentId : ''; if (!agentId) return true; const v = data.override; const override = v === null ? null : { persona: typeof v?.persona === 'string' ? v.persona : undefined, specialty: typeof v?.specialty === 'string' ? v.specialty : undefined, tagline: typeof v?.tagline === 'string' ? v.tagline : undefined, }; await setAgentPromptOverride(provider._context, agentId, override); await provider._sendCompanyAgents(); return true; } case 'proactiveTrigger': await provider._handleProactiveSuggestion(data.context); return true; case 'exportResponse': { const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath || ''; const defaultPath = path.join(workspacePath, 'g1_response.md'); const uri = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(defaultPath), filters: { 'Markdown': ['md'] } }); if (uri) { await vscode.workspace.fs.writeFile(uri, Buffer.from(data.text, 'utf8')); vscode.window.showInformationMessage(`✅ Exported to ${path.basename(uri.fsPath)}`); } return true; } case 'approveAction': await provider._agent.approveTransaction(); return true; case 'rejectAction': await provider._agent.rejectTransaction(); return true; default: return false; } }