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': console.error(`[ASTRA-DEBUG] prompt case entered type=${data?.type} value=${JSON.stringify(String(data?.value ?? '').slice(0, 80))}`); provider._lmStudio?.activity.bump(); // โ”€โ”€ ๐Ÿ“ป Datacollect Radio (slash ๋ช…๋ น) ์šฐ์„  ๋ถ„๊ธฐ โ”€โ”€ // ์ฃผ์˜: globalState.update๋ณด๋‹ค *๋จผ์ €* ์žก๋Š”๋‹ค โ€” ๊ธ€๋กœ๋ฒŒ state๊ฐ€ ~1MB๊นŒ์ง€ // ๋ˆ„์ ๋œ ํ™˜๊ฒฝ์—์„œ update๊ฐ€ ๋А๋ ค ์ฒซ prompt๊ฐ€ hangํ•˜๋Š” ์‚ฌ๋ก€ ๋ณด๊ณ ๋จ. slash // ๋ช…๋ น์€ LLM์„ ์šฐํšŒํ•˜๋‹ˆ blank chat state ๊ฐฑ์‹ ๋„ ํ•„์š” ์—†์Œ. if (typeof data.value === 'string') { const { isSlashCommand, handleSlashCommand } = await import('../features/datacollect/slashRouter'); const matched = isSlashCommand(data.value); console.error(`[ASTRA-DEBUG] slash check matched=${matched} hasView=${!!provider._view}`); logInfo(`[SLASH] prompt received: ${JSON.stringify(data.value).slice(0, 100)} matched=${matched} hasView=${!!provider._view}`); if (matched) { if (!provider._view?.webview) { const msg = '๐Ÿ“ป Datacollect Radio: ์ฑ„ํŒ… webview๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. Astra ์‚ฌ์ด๋“œ๋ฐ”๋ฅผ ํ•œ ๋ฒˆ ์—ด๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.'; await vscode.window.showWarningMessage(msg); logInfo(`[SLASH] webview not available โ€” aborting`); return true; } logInfo(`[SLASH] handleSlashCommand entering`); await handleSlashCommand(data.value, provider._view.webview); logInfo(`[SLASH] handleSlashCommand returned`); return true; } } await provider._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); // โ”€โ”€ 1์ธ ๊ธฐ์—… ๋ชจ๋“œ ์šฐ์„  ๋ถ„๊ธฐ โ”€โ”€ // ํšŒ์‚ฌ ๋ชจ๋“œ์ผ ๋• ๋“ค์–ด์˜จ ๋ฉ”์‹œ์ง€๋ฅผ intent classifier์— ํ•œ ๋ฒˆ ํ†ต๊ณผ์‹œ์ผœ // (a) ์žก๋‹ด/์งˆ๋ฌธ/์งง์€ ์‘๋‹ต โ†’ ์ผ๋ฐ˜ ์ฑ„ํŒ… ๊ฒฝ๋กœ, (b) ํ›„์† ๋Œ€ํ™” โ†’ ์ผ๋ฐ˜ ์ฑ„ํŒ… // ๊ฒฝ๋กœ + followup ๋ผ๋ฒจ, (c) ์‹ ๊ทœ ์—…๋ฌด โ†’ ํ’€ ํŒŒ์ดํ”„๋ผ์ธ dispatch. // classifier๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋ผ ์žˆ๊ฑฐ๋‚˜ ํ˜ธ์ถœ ์‹คํŒจ ์‹œ์—” ์•ˆ์ „ํ•˜๊ฒŒ new_task๋กœ // ํด๋ฐฑ โ€” ์ฆ‰ ์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•œ ์ž‘์—… ์š”์ฒญ์„ ๋†“์น˜๋Š” ์ผ์€ ์ ˆ๋Œ€ ์—†๋‹ค. if (provider.isCompanyModeEnabled() && typeof data.value === 'string' && data.value.trim()) { let userPrompt = data.value.trim(); const { getConfig } = await import('../config'); const cfg = getConfig(); const { readCompanyState, resolveActivePipeline } = await import('../features/company'); const state = readCompanyState(provider._context); // โ”€โ”€ alignment ๋‹ต๋ณ€ ๋ผ์šฐํŒ… โ”€โ”€ // ์‚ฌ์šฉ์ž๊ฐ€ ์ด์ „ ๋ฉ”์‹œ์ง€์—์„œ alignment ์นด๋“œ๋ฅผ ๋ฐ›์•„ ๋‹ต๋ณ€ํ•˜๋Š” ์ค‘์ด๋ฉด // ์ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ถ„๋ฅ˜๊ธฐ/dispatch๊ฐ€ ์•„๋‹ˆ๋ผ alignment ๋‹ต๋ณ€ ํ•ธ๋“ค๋Ÿฌ๋กœ // ๋ณด๋‚ธ๋‹ค. ๋‹ต๋ณ€์ด ์ฒ˜๋ฆฌ๋˜๋ฉด์„œ ์ž๋™์œผ๋กœ ๋‹ค์Œ ๋ผ์šด๋“œ ๋˜๋Š” pipeline // ์œผ๋กœ ์ง„ํ–‰๋จ. if (provider.isAlignmentPending()) { await provider._handleAlignmentAnswer(userPrompt); return true; } // โ”€โ”€ ์‚ฌ์šฉ์ž ํ‚ค์›Œ๋“œ override โ”€โ”€ // ์ž…๋ ฅ ๋งจ ์•ž์— `[ํŒŒ์ดํ”„๋ผ์ธ:id]` ๋˜๋Š” `[pipeline:id]`๊ฐ€ ์žˆ์œผ๋ฉด // ๋ถ„๋ฅ˜๊ธฐ ๋ฌด๊ด€ํ•˜๊ฒŒ ๊ทธ ํŒŒ์ดํ”„๋ผ์ธ ๊ฐ•์ œ + ๊ทธ ํ‚ค์›Œ๋“œ๋Š” prompt์—์„œ // ์ œ๊ฑฐ ํ›„ dispatcher์— ์ „๋‹ฌ. id๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ๋ฌด์‹œ(๋ถ„๋ฅ˜๊ธฐ ์ •์ƒ ๊ฒฝ๋กœ). let keywordOverrideId: string | undefined; const keywordMatch = userPrompt.match(/^\s*\[(?:ํŒŒ์ดํ”„๋ผ์ธ|pipeline)\s*:\s*([a-z0-9_-]+)\s*\]\s*/i); if (keywordMatch) { const id = keywordMatch[1].toLowerCase(); if (state.pipelines?.[id]) { keywordOverrideId = id; userPrompt = userPrompt.slice(keywordMatch[0].length).trim() || userPrompt; } } // โ”€โ”€ alignment bypass ํ‚ค์›Œ๋“œ โ”€โ”€ // ์ž…๋ ฅ ๋งจ ์•ž `[๊ฑด๋„ˆ๋›ฐ๊ธฐ]` ๋˜๋Š” `[skip]` โ†’ alignment ๋‹จ๊ณ„ 1ํšŒ ์šฐํšŒ. // ์‚ฌ์šฉ์ž๊ฐ€ "์ง€๊ธˆ์€ ๋นจ๋ฆฌ ๊ฐ€์ž"๋ผ๊ณ  ๋ช…์‹œํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉ. prompt์—์„œ // ํ‚ค์›Œ๋“œ ์ œ๊ฑฐ. let alignmentBypass = false; const bypassMatch = userPrompt.match(/^\s*\[(?:๊ฑด๋„ˆ๋›ฐ๊ธฐ|skip)\]\s*/i); if (bypassMatch) { alignmentBypass = true; userPrompt = userPrompt.slice(bypassMatch[0].length).trim() || userPrompt; } if (cfg.companyDisableIntentClassifier) { // ๋ถ„๋ฅ˜๊ธฐ ์šฐํšŒ ๋ชจ๋“œ โ€” ๋ถ„๋ฅ˜ ๋‹จ๊ณ„๋Š” ๊ฑด๋„ˆ๋›ฐ์ง€๋งŒ alignment๋Š” ๋ณ„๋„๋กœ // ์ž‘๋™(์‚ฌ์šฉ์ž๊ฐ€ alignment off๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์€ ํ•œ). ๋ถ„๋ฅ˜๊ธฐ ๋„๋Š” // ์ด์œ ๋Š” ๋ณดํ†ต "์žก๋‹ด๋„ ๋‹ค pipeline์œผ๋กœ"์ธ๋ฐ ๊ทธ๋Ÿด์ˆ˜๋ก alignment // ํšจ๊ณผ๊ฐ€ ํผ. try { provider.pixelOfficeOnIntentClassified('new_task', userPrompt); } catch { /* noop */ } if (cfg.companyIntentAlignmentMode === 'off' || alignmentBypass) { await provider._runCompanyTurn(userPrompt, undefined, keywordOverrideId); } else { await provider._runIntentAlignment({ userPrompt, pipelineIdOverride: keywordOverrideId, mode: cfg.companyIntentAlignmentMode === 'strict' ? 'strict' : 'smart', roundsLimit: cfg.companyIntentAlignmentMaxRounds, roundsAsked: 0, }); } return true; } const { classifyChatIntent } = await import('../features/company'); const { AIService } = await import('../core/services'); const last = provider.getLastCompanyTurnSummary(); const activePipeline = resolveActivePipeline(state); // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ํŒŒ์ดํ”„๋ผ์ธ์„ ๋ถ„๋ฅ˜๊ธฐ ํ›„๋ณด๋กœ ์ „๋‹ฌ โ€” ๋‹จ, ํ™œ์„ฑํ™”๋ผ // ์žˆ์–ด์•ผ ์ถ”์ฒœ ์˜๋ฏธ๊ฐ€ ์žˆ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ *์ •์˜๋ผ ์žˆ๊ธฐ๋งŒ ํ•˜๋ฉด* ํ›„๋ณด. ์‚ฌ์šฉ์ž๊ฐ€ // ํ‰์†Œ์—” ์งง์€ ๊ฑธ ํ™œ์„ฑํ™”ํ•ด ๋‘๊ณ  ๊ฐ€๋” ํ’€ ์‚ฌ์ดํด ์˜๋„๊ฐ€ ๋ช…ํ™•ํ•œ ๋ฐœํ™”๋ฅผ // ํ–ˆ์„ ๋•Œ ๋ถ„๋ฅ˜๊ธฐ๊ฐ€ ๊ทธ์ชฝ์„ ์ถ”์ฒœํ•  ์ˆ˜ ์žˆ๊ฒŒ. const allPipelines = Object.values(state.pipelines ?? {}); const verdict = await classifyChatIntent( new AIService(), userPrompt, { previousBrief: last?.brief, previousReportTail: last?.reportTail, previousTurnAt: last?.finishedAt, activePipelineName: activePipeline?.name, availablePipelines: allPipelines.length > 0 ? allPipelines.map((p) => ({ id: p.id, name: p.name, stageCount: p.stages.length, })) : undefined, }, { model: cfg.companyIntentClassifierModel || cfg.defaultModel }, ); // Pixel Office: ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ๋ฅผ UI layer๋กœ๋งŒ ํ˜๋ฆผ. ์•„๋ž˜ ๋ถ„๊ธฐ ์ž์ฒด์—” ์˜ํ–ฅ ์—†์Œ. try { provider.pixelOfficeOnIntentClassified(verdict.intent, userPrompt); } catch { /* noop */ } if (verdict.intent === 'new_task') { // ์šฐ์„ ์ˆœ์œ„: (1) ์‚ฌ์šฉ์ž ํ‚ค์›Œ๋“œ (2) autoSelect๊ฐ€ ์ผœ์ ธ ์žˆ๊ณ  ๋ถ„๋ฅ˜๊ธฐ ์ถ”์ฒœ ์žˆ์Œ (3) ์‚ฌ์šฉ์ž ํ™œ์„ฑ ํŒŒ์ดํ”„๋ผ์ธ. let effectiveOverride = keywordOverrideId; if (!effectiveOverride && cfg.companyAutoSelectPipeline && verdict.suggestedPipelineId) { effectiveOverride = verdict.suggestedPipelineId; } // ๋ถ„๋ฅ˜๊ธฐ๊ฐ€ ์ถ”์ฒœ์„ ๋ƒˆ์ง€๋งŒ autoSelect๊ฐ€ ๊บผ์ ธ ์žˆ์„ ๋• ๋ผ๋ฒจ๋กœ๋งŒ ์•ˆ๋‚ด. if (verdict.suggestedPipelineId && !effectiveOverride && !cfg.companyAutoSelectPipeline) { const tip = state.pipelines?.[verdict.suggestedPipelineId]; if (tip) { provider._view?.webview.postMessage({ type: 'companyIntentDecision', value: { intent: 'new_task', reason: `๐Ÿงญ ์ถ”์ฒœ ํŒŒ์ดํ”„๋ผ์ธ: "${tip.name}" (์ž๋™ ์ ์šฉ์€ ์„ค์ • ํ† ๊ธ€)`, label: '๐Ÿ› ๏ธ ์‹ ๊ทœ ์—…๋ฌด', }, }); } } else if (effectiveOverride && effectiveOverride !== state.activePipelineId) { const used = state.pipelines?.[effectiveOverride]; if (used) { provider._view?.webview.postMessage({ type: 'companyIntentDecision', value: { intent: 'new_task', reason: keywordOverrideId ? `๐Ÿ”ง ํ‚ค์›Œ๋“œ override โ†’ "${used.name}"` : `๐Ÿงญ CEO ์ž๋™ ์„ ํƒ โ†’ "${used.name}"`, label: '๐Ÿ› ๏ธ ์‹ ๊ทœ ์—…๋ฌด', }, }); } } // โ”€โ”€ Intent Alignment ์ง„์ž… โ”€โ”€ // off ๋ชจ๋“œ์ด๊ฑฐ๋‚˜ bypass ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด alignment ์šฐํšŒํ•˜๊ณ  // legacy ๋™์ž‘ (์ฆ‰์‹œ dispatch). ๊ทธ ์™ธ์—” ๋ถ„์„๊ธฐ 1๋ผ์šด๋“œ ๋Œ๋ ค // confidence์— ๋”ฐ๋ผ ์ž๋™ ์ง„ํ–‰ ๋˜๋Š” ์นด๋“œ ํ‘œ์‹œ. if (cfg.companyIntentAlignmentMode === 'off' || alignmentBypass) { await provider._runCompanyTurn(userPrompt, undefined, effectiveOverride); } else { await provider._runIntentAlignment({ userPrompt, pipelineIdOverride: effectiveOverride, mode: cfg.companyIntentAlignmentMode === 'strict' ? 'strict' : 'smart', roundsLimit: cfg.companyIntentAlignmentMaxRounds, roundsAsked: 0, }); } } else { await provider._handleCompanyCasual(userPrompt, verdict.intent, verdict.reason, data); } return true; } try { await provider._handlePrompt(data); await provider._autoWriteChronicleAfterPrompt(); } finally { // Persist the session even if _handlePrompt or the chronicle // auto-write throws *after* the answer already streamed โ€” // otherwise the reply shows in the UI but never lands in the // ๊ธฐ๋ก(Chat History) list. This is the regression that made // recent conversations stop appearing. 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(); // Pixel Office โ€” ์ฒซ ๋กœ๋“œ ์‹œ ๋นˆ idle ์ƒํƒœ๋ผ๋„ ํ•œ ๋ฒˆ pushํ•ด์„œ webview๊ฐ€ // ์˜์—ญ ์ž์ฒด๋ฅผ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๊ฒŒ. provider.pixelOfficeResend(); 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); // ์ง์ „ ํšŒ์‚ฌ turn ์ปจํ…์ŠคํŠธ ์บ์‹œ ๋น„์šฐ๊ธฐ โ€” ์ƒˆ ์„ธ์…˜์€ followup ๊ธฐ์ค€์ ์ด ์—†๋‹ค. provider.clearLastCompanyTurnSummary(); // ์ง„ํ–‰ ์ค‘์ด๋˜ alignment๋„ ์ƒˆ ์„ธ์…˜๊ณผ ํ•จ๊ป˜ ํ๊ธฐ. provider.cancelPendingAlignment(); provider.clearChat(); await provider._sendBrainStatus(); return true; case 'stopGeneration': // 1์ธ ๊ธฐ์—… ๋ชจ๋“œ๋Š” AgentExecutor๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ณ„๋„ abort ๊ฒฝ๋กœ. // ๋‘ ๊ฒฝ๋กœ ๋ชจ๋‘ ์‹ ํ˜ธ๋ฅผ ๋ณด๋‚ด ๋‘๋ฉด ์ค‘๊ฐ„์— ๋ชจ๋“œ ์ „ํ™˜๋˜์–ด๋„ ์•ˆ์ „. provider.abortCompanyTurn(); // ์ง„ํ–‰ ์ค‘์ธ Intent Alignment๋„ ๊ฐ™์ด ์ •๋ฆฌ โ€” ์‚ฌ์šฉ์ž๊ฐ€ Stop ๋ˆ„๋ฅด๋ฉด // ์˜๋„์ƒ ๋ชจ๋“  ๋Œ€๊ธฐ ์ƒํƒœ ํ•ด์ œ. provider.cancelPendingAlignment(); provider._agent.stop(); return true; case 'loadSession': // ์„ธ์…˜ ์ „ํ™˜ ์‹œ ์ง์ „ ํšŒ์‚ฌ turn ์บ์‹œ ๋ฌดํšจํ™” โ€” ๋กœ๋“œ๋œ ์„ธ์…˜์˜ ์ง์ „ ํšŒ์‚ฌ // ์ž‘์—…์€ ๋ณ„๋„ ํŒŒ์ผ์—์„œ ๋ณต์›๋˜์–ด์•ผ ํ•˜์ง€ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์— ๋‚จ์€ ๋‹ค๋ฅธ // ์„ธ์…˜์˜ ๋ณด๊ณ ์„œ๊ฐ€ ์ƒˆ ์„ธ์…˜ ์ฒซ ๋ฉ”์‹œ์ง€์˜ followup ํŒ์ •์„ ์˜ค์—ผ์‹œํ‚ค๋ฉด ์•ˆ ๋จ. provider.clearLastCompanyTurnSummary(); provider.cancelPendingAlignment(); 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 'attachArchitecture': // Re-enable architecture context for the current workspace โ€” // user clicked the inactive chip's [Attach] button. await provider._attachArchitecture(); 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 'getCompanyResumable': await provider._sendCompanyResumable(); return true; case 'resumeCompanyTurn': { // ์‚ฌ์šฉ์ž๊ฐ€ "์ด์–ด์„œ ์ง„ํ–‰" ์นฉ์„ ๋ˆŒ๋ €์„ ๋•Œ. timestamp๋งŒ ๋ฐ›์•„์„œ ๋””์Šคํฌ์˜ // _resume.json์„ ์ฝ๊ณ  ๊ทธ ๋‹ค์Œ stage๋ถ€ํ„ฐ dispatch๊ฐ€ ์ด์–ด์ง„๋‹ค. const ts = typeof data.timestamp === 'string' ? data.timestamp : ''; if (!ts) return true; // userPrompt ์ธ์ž๋Š” resume ๊ฒฝ๋กœ์—์„œ ๋ฌด์‹œ๋˜์ง€๋งŒ(plan์€ ๋””์Šคํฌ์—์„œ ๋ณต์›) // ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด dummy ๊ฐ’์„ ์ „๋‹ฌ. void provider._runCompanyTurn('', ts); return true; } case 'discardResumableSession': { // ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์žฌ๊ฐœ ํ•ญ๋ชฉ์„ ๋ฒ„๋ฆฌ๊ณ  ์‹ถ์„ ๋•Œ โ€” resume ํŒŒ์ผ์„ 'failed'๋กœ // ๋งˆํ‚นํ•ด์„œ listResumable์—์„œ ์ž๋™ ์ œ์™ธ. markResumeStatus๊ฐ€ ์•ˆ์ „ํ•œ idempotent // ์ž‘์—…์ด๋ผ ๋ณ„๋„ ๊ฒ€์ฆ ๋ถˆํ•„์š”. const ts = typeof data.timestamp === 'string' ? data.timestamp : ''; if (!ts) return true; try { const { resolveSessionDir } = await import('../features/company'); const { markResumeStatus } = await import('../features/company/resumeStore'); markResumeStatus(resolveSessionDir(provider._context, ts), 'failed', 'discarded-by-user'); } catch { /* ๋ฌด์‹œ โ€” ๋‹ค์Œ ํ‘ธ์‹œ์—์„œ ์ž์—ฐ ๋ณต๊ตฌ */ } await provider._sendCompanyResumable(); 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 'setCompanyAgentDisplay': { // ์ด๋ฆ„/์—ญํ• /์ด๋ชจ์ง€/์ƒ‰์ƒ override. ํŽ˜์ด๋กœ๋“œ๋Š” setCompanyAgentPrompt์™€ // ๋™์ผํ•œ ํŒจํ„ด โ€” null์ด๋ฉด ์ „์ฒด reset, ๊ฐ ํ•„๋“œ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ๊ทธ ํ•„๋“œ๋งŒ reset. const { setAgentDisplayOverride } = 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 : { name: typeof v?.name === 'string' ? v.name : undefined, role: typeof v?.role === 'string' ? v.role : undefined, emoji: typeof v?.emoji === 'string' ? v.emoji : undefined, color: typeof v?.color === 'string' ? v.color : undefined, }; const result = await setAgentDisplayOverride(provider._context, agentId, override); provider._view?.webview.postMessage({ type: 'setCompanyAgentDisplayResult', value: result.ok ? { ok: true, agentId } : { ok: false, reason: result.reason }, }); if (result.ok) await provider._sendCompanyAgents(); return true; } case 'setCompanyAgentRoleCategory': { // Override an agent's ์ง๊ตฐ. Empty / null payload value reverts to // the def's own roleCategory. CEO is rejected by the backend. const { setAgentRoleCategory } = await import('../features/company'); const agentId = typeof data.agentId === 'string' ? data.agentId : ''; if (!agentId) return true; const cat = (typeof data.value === 'string' && data.value.trim()) ? data.value.trim() : null; const result = await setAgentRoleCategory(provider._context, agentId, cat as any); provider._view?.webview.postMessage({ type: 'setCompanyAgentRoleCategoryResult', value: result.ok ? { ok: true, agentId } : { ok: false, reason: result.reason }, }); if (result.ok) await provider._sendCompanyAgents(); return true; } case 'setCompanyAgentKnowledgeMix': { // Per-agent Knowledge Mix override. `null`/missing value falls // back to the global slider. The dispatcher reads this on the // *next* turn โ€” no restart required. const { setAgentKnowledgeMix } = await import('../features/company'); const agentId = typeof data.agentId === 'string' ? data.agentId : ''; if (!agentId) return true; const raw = data.value; const weight = (raw === null || raw === undefined || !Number.isFinite(Number(raw))) ? null : Math.max(0, Math.min(100, Math.round(Number(raw)))); await setAgentKnowledgeMix(provider._context, agentId, weight); 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 'addCompanyAgent': { // User-defined agent. Payload: { def: CompanyAgentDef }. Returns // an `addCompanyAgentResult` so the UI overlay can keep its form // open + show an error when validation fails (id collision etc.). const { addCustomAgent } = await import('../features/company'); const def = data.def; const result = await addCustomAgent(provider._context, def ?? {}); provider._view?.webview.postMessage({ type: 'addCompanyAgentResult', value: result.ok ? { ok: true, agentId: def?.id } : { ok: false, reason: result.reason }, }); if (result.ok) { await provider._sendCompanyStatus(); await provider._sendCompanyAgents(); } return true; } case 'deleteCompanyAgent': { // Delete any agent (built-in via hide, custom via outright removal). // Backend checks pipeline usage and refuses if any stage references it. const { removeCompanyAgent } = await import('../features/company'); const agentId = typeof data.agentId === 'string' ? data.agentId : ''; if (!agentId) return true; const result = await removeCompanyAgent(provider._context, agentId); provider._view?.webview.postMessage({ type: 'deleteCompanyAgentResult', value: result.ok ? { ok: true, agentId, kind: result.kind } : { ok: false, agentId, reason: result.reason }, }); if (result.ok) { await provider._sendCompanyStatus(); await provider._sendCompanyAgents(); } return true; } case 'restoreHiddenAgent': { // Bring a previously-hidden built-in back into the manage panel. const { restoreHiddenAgent } = await import('../features/company'); const agentId = typeof data.agentId === 'string' ? data.agentId : ''; if (!agentId) return true; const result = await restoreHiddenAgent(provider._context, agentId); provider._view?.webview.postMessage({ type: 'restoreHiddenAgentResult', value: result.ok ? { ok: true, agentId } : { ok: false, reason: result.reason }, }); if (result.ok) { await provider._sendCompanyStatus(); await provider._sendCompanyAgents(); } return true; } case 'getCompanyPipelines': await provider._sendCompanyPipelines(); return true; case 'upsertCompanyPipeline': { const { upsertPipeline } = await import('../features/company'); const result = await upsertPipeline(provider._context, data.def ?? {}); provider._view?.webview.postMessage({ type: 'upsertCompanyPipelineResult', value: result.ok ? { ok: true } : { ok: false, reason: result.reason }, }); if (result.ok) await provider._sendCompanyPipelines(); return true; } case 'deleteCompanyPipeline': { const { deletePipeline } = await import('../features/company'); const pid = typeof data.pipelineId === 'string' ? data.pipelineId : ''; if (!pid) return true; const result = await deletePipeline(provider._context, pid); provider._view?.webview.postMessage({ type: 'deleteCompanyPipelineResult', value: result.ok ? { ok: true, pipelineId: pid } : { ok: false, reason: result.reason }, }); if (result.ok) await provider._sendCompanyPipelines(); return true; } case 'getCompanyPipelineTemplate': { // Returns a template's stages so the editor can pre-fill the form. const { getPipelineTemplate } = await import('../features/company'); const tplId = typeof data.templateId === 'string' ? data.templateId : ''; const tpl = getPipelineTemplate(tplId); provider._view?.webview.postMessage({ type: 'companyPipelineTemplateContent', value: tpl ? { templateId: tpl.templateId, suggestedPipelineId: tpl.suggestedPipelineId, suggestedPipelineName: tpl.suggestedPipelineName, stages: tpl.stages, } : null, }); return true; } case 'getPixelOfficeState': // webview๊ฐ€ ์ฒ˜์Œ ๋กœ๋“œ๋˜์—ˆ๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž๊ฐ€ ํ† ๊ธ€ ON ํ–ˆ์„ ๋•Œ ์บ์‹œ๋œ // ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ๋ฐ›๊ธฐ ์œ„ํ•œ ์š”์ฒญ. read-only. provider.pixelOfficeResend(); return true; case 'openPixelOfficePanel': // ์‚ฌ์ด๋“œ๋ฐ” mini Pixel Office์˜ โ›ถ ๋ฒ„ํŠผ โ†’ editor area์— ์ „์ฒด๋ณด๊ธฐ panel ์—ด๊ธฐ. provider.openPixelOfficePanel(); return true; case 'respondCompanyAlignment': { // alignment ์นด๋“œ ๋ฒ„ํŠผ: 'proceed' = ํ˜„ contract๋กœ dispatch, 'cancel' = ํ๊ธฐ. const decision = typeof data.decision === 'string' ? data.decision : ''; if (decision === 'proceed') { await provider._proceedWithCurrentAlignment(); } else if (decision === 'cancel') { provider.cancelPendingAlignment(); } return true; } case 'respondCompanyApproval': { // Webview์˜ ์Šน์ธ ์นด๋“œ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ dispatcher์˜ await ํ•ด์ œ. // payload: { stageId, decision: 'approve' | 'revise' | 'abort', comment? } const stageId = typeof data.stageId === 'string' ? data.stageId : ''; const decision = typeof data.decision === 'string' ? data.decision : ''; if (!stageId || !['approve', 'revise', 'abort'].includes(decision)) return true; let payload: any; if (decision === 'approve') payload = { kind: 'approve' }; else if (decision === 'abort') payload = { kind: 'abort' }; else payload = { kind: 'revise', comment: typeof data.comment === 'string' ? data.comment : '' }; provider.resolveApprovalGate(stageId, payload); return true; } case 'setActiveCompanyPipeline': { const { setActivePipeline } = await import('../features/company'); const pid = typeof data.pipelineId === 'string' && data.pipelineId.trim() ? data.pipelineId.trim() : null; const result = await setActivePipeline(provider._context, pid); provider._view?.webview.postMessage({ type: 'setActiveCompanyPipelineResult', value: result.ok ? { ok: true, pipelineId: pid } : { ok: false, reason: result.reason }, }); if (result.ok) await provider._sendCompanyPipelines(); return true; } case 'setCompanyScopePreset': { // ์Šค์ฝ”ํ”„ ํ”„๋ฆฌ์…‹ ํด๋ฆญ โ€” templateId (plan-only / dev-only / full-product-dev) ๋ฐ›์•„์„œ: // 1) suggestedPipelineId ์˜ pipeline ์ด state.pipelines ์— ์—†์œผ๋ฉด template stamp // 2) activePipelineId ๋ฅผ ๊ทธ id ๋กœ ์„ค์ • // ์ด๋ฏธ stamp ๋œ pipeline ์ด๋ผ๋ฉด stage ์‚ฌ์šฉ์ž ํŽธ์ง‘์„ *์œ ์ง€* (ํ™œ์„ฑํ™”๋งŒ). const { getPipelineTemplate, upsertPipeline, setActivePipeline, readCompanyState } = await import('../features/company'); const tplId = typeof data.templateId === 'string' ? data.templateId : ''; const tpl = getPipelineTemplate(tplId); if (!tpl) { provider._view?.webview.postMessage({ type: 'setCompanyScopePresetResult', value: { ok: false, reason: `์•Œ ์ˆ˜ ์—†๋Š” ํ…œํ”Œ๋ฆฟ: ${tplId}` }, }); return true; } const state = readCompanyState(provider._context); if (!state.pipelines || !state.pipelines[tpl.suggestedPipelineId]) { const stampDef = { id: tpl.suggestedPipelineId, name: tpl.suggestedPipelineName, // stage ๋Š” deep clone โ€” ํ…œํ”Œ๋ฆฟ read-only ์›๋ณธ ๋ณดํ˜ธ. stages: tpl.stages.map((s) => ({ ...s })), }; const stamp = await upsertPipeline(provider._context, stampDef); if (!stamp.ok) { provider._view?.webview.postMessage({ type: 'setCompanyScopePresetResult', value: { ok: false, reason: stamp.reason }, }); return true; } } const activate = await setActivePipeline(provider._context, tpl.suggestedPipelineId); provider._view?.webview.postMessage({ type: 'setCompanyScopePresetResult', value: activate.ok ? { ok: true, pipelineId: tpl.suggestedPipelineId, templateId: tplId } : { ok: false, reason: activate.reason }, }); if (activate.ok) { await provider._sendCompanyStatus(); await provider._sendCompanyPipelines(); } 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; } }