358 lines
18 KiB
TypeScript
358 lines
18 KiB
TypeScript
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<boolean> {
|
|
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':
|
|
// 1인 기업 모드는 AgentExecutor를 거치지 않으므로 별도 abort 경로.
|
|
// 두 경로 모두 신호를 보내 두면 중간에 모드 전환되어도 안전.
|
|
provider.abortCompanyTurn();
|
|
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<number>('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 '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 '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': {
|
|
// Drop a user-defined agent. Built-ins refuse — backend enforces.
|
|
const { removeCustomAgent } = await import('../features/company');
|
|
const agentId = typeof data.agentId === 'string' ? data.agentId : '';
|
|
if (!agentId) return true;
|
|
const result = await removeCustomAgent(provider._context, agentId);
|
|
provider._view?.webview.postMessage({
|
|
type: 'deleteCompanyAgentResult',
|
|
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 '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 '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;
|
|
}
|
|
}
|