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': logInfo(`[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); logInfo(`[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`); // Slash λͺ…λ Ή κ²°κ³Όλ₯Ό *chatHistory 에도* mirror β€” λ‹€μŒ turn 의 LLM 이 // 직전 λͺ…λ Ή 좜λ ₯ (예: /stocks discover 발꡴ κ²°κ³Ό) 을 μ»¨ν…μŠ€νŠΈλ‘œ 보게 ν•œλ‹€. // μ•ˆ ν•˜λ©΄ LLM 은 webview UI 만 보고 history λŠ” λΉ„μ–΄ μžˆμ–΄μ„œ "주식 데이터 // μ—†μŒ" 같은 ν™˜κ° 응닡이 λ‚˜μ˜΄. const realWebview = provider._view.webview; const captured: string[] = []; const captureWebview = { postMessage: (msg: any) => { if (msg && msg.type === 'streamChunk' && typeof msg.value === 'string') { captured.push(msg.value); } return realWebview.postMessage(msg); }, }; await handleSlashCommand(data.value, captureWebview as any, provider._context); const collected = captured.join('').trim(); if (collected) { // CRITICAL: getHistory() λŠ” *filtered copy* λ₯Ό λ°˜ν™˜ν•˜λ―€λ‘œ 직접 push ν•˜λ©΄ // 원본 chatHistory 에 μ•ˆ 듀어감 (silent fail). μƒˆ λ°°μ—΄λ‘œ setHistory. const current = provider._agent.getHistory(); const updated = [ ...current, { role: 'user' as const, content: data.value }, { role: 'assistant' as const, content: collected, internal: false }, ]; provider._agent.setHistory(updated); } logInfo(`[SLASH] handleSlashCommand returned, captured ${collected.length} chars β†’ history len ${provider._agent.getHistory().length}`); return true; } } await provider._sessionState.setBlankChatActive(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 // νš¨κ³Όκ°€ 큼. 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 }, ); 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(); // Slash λͺ…λ Ή λͺ©λ‘ β€” webview 의 `/` μžλ™μ™„μ„± dropdown 이 μ‚¬μš©. try { const { listSlashCommands } = await import('../features/datacollect/slashRouter'); const cmds = listSlashCommands().map(c => ({ name: c.name, description: c.description || '' })); provider._view?.webview.postMessage({ type: 'slashCommandList', commands: cmds }); } catch (e: any) { // Slash λΌμš°ν„° λ‘œλ“œ μ‹€νŒ¨ν•΄λ„ μ±„νŒ… μžμ²΄λŠ” λ™μž‘ν•΄μ•Ό β€” silent skip. } // 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._sessionState.setActiveSessionId(null); await provider._sessionState.setLastVisibleChat(null); await provider._sessionState.setBlankChatActive(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 }); // Wipe any persistent LM Studio error segment from the readyBar β€” the // next load attempt will repaint it if it also fails. provider.clearLmStudioError(); 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인 κΈ°μ—… λͺ¨λ“œ λ©”μ‹œμ§€ λΌμš°νŒ… ──────────────────────────────────── // 별도 도메인 ν•Έλ“€λŸ¬μ— μœ„μž„. 30+ 개의 νšŒμ‚¬ λͺ¨λ“œ λ©”μ‹œμ§€κ°€ ν•œ 파일둜 λΆ€ν’€μ–΄ // SRP κ°€ 깨쑌던 κ±Έ 뢄리. 처리 λͺ» ν•œ 경우 false λ°˜ν™˜ β†’ μ•„λž˜ default 둜 흐름. case 'getCompanyStatus': case 'getCompanyAgents': case 'getCompanyResumable': case 'discardResumableSession': case 'setCompanyEnabled': case 'setCompanyName': case 'setCompanyActiveAgents': case 'setCompanyAgentModel': case 'setCompanyAgentDisplay': case 'setCompanyAgentRoleCategory': case 'setCompanyAgentKnowledgeMix': case 'setCompanyAgentPrompt': case 'addCompanyAgent': case 'deleteCompanyAgent': case 'restoreHiddenAgent': case 'getCompanyPipelines': case 'upsertCompanyPipeline': case 'deleteCompanyPipeline': case 'getCompanyPipelineTemplate': case 'respondCompanyAlignment': case 'respondCompanyApproval': case 'setActiveCompanyPipeline': case 'setCompanyScopePreset': case 'resumeCompanyTurn': { const { handleCompanyMessage } = await import('./companyHandlers'); return await handleCompanyMessage(provider, data); } 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; } }