chore: bump version to v2.33.4 and apply manual stabilization updates

This commit is contained in:
g1nation
2026-05-01 19:23:30 +09:00
parent ddb96e6407
commit fad4725008
7 changed files with 387 additions and 87 deletions
+9
View File
@@ -1,3 +1,12 @@
# Patch Notes - v2.33.4 (2026-05-01)
## 🛠️ Manual Refinement & Stabilization
- **Core Agent Logic Update:** Manually refined `agent.ts` and `AgentWorkflowManager.ts` for improved task orchestration.
- **Workflow Factory Optimization:** Updated `factory.ts` to support more robust agent instantiation.
- **Configuration & UI Polish:** Fine-tuned `config.ts` and `sidebarProvider.ts` for a better user experience.
---
# Patch Notes - v2.32.0 (2026-04-30)
## 🏛️ Modernization: Actor/Queue Model & Monitoring
+71 -1
View File
@@ -2,7 +2,7 @@
"name": "g1nation",
"displayName": "G1nation",
"description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.",
"version": "2.33.3",
"version": "2.33.4",
"publisher": "connectailab",
"license": "MIT",
"icon": "assets/icon.png",
@@ -100,6 +100,76 @@
"default": true,
"description": "Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks."
},
"g1nation.multiAgentWorkflow": {
"type": "array",
"default": [
{
"id": "planner",
"name": "Planner",
"input": "original",
"prompt": "You are the [Master Strategist & Planner].\nTransform the user request into a precise execution blueprint.\nOutput Markdown with Objective, Core Challenges, Data Requirements, and Step-by-Step Tasks."
},
{
"id": "researcher",
"name": "Researcher",
"input": "combined",
"prompt": "You are the [Senior Technical Researcher].\nExtract, filter, and synthesize high-signal facts from the plan and available context.\nOutput Key Facts, Technical Deep-Dive, Gaps, and Summary of Knowledge."
},
{
"id": "writer",
"name": "Writer",
"input": "combined",
"prompt": "You are the [Lead Synthesis Writer & Editor].\nProduce the final response in the user language with a clear conclusion and actionable recommendation.\nPreserve important technical nuance without padding."
}
],
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Stable stage id."
},
"name": {
"type": "string",
"description": "Stage label shown in progress UI."
},
"input": {
"type": "string",
"enum": ["original", "previous", "combined"],
"default": "combined",
"description": "Which input this stage receives: original user request, previous stage output, or both."
},
"prompt": {
"type": "string",
"description": "Role and instructions for this stage."
}
}
},
"description": "Configurable Multi-Agent workflow. Add, remove, reorder, or rewrite stages in settings JSON."
},
"g1nation.memoryEnabled": {
"type": "boolean",
"default": true,
"description": "Enable layered memory injection before each model response."
},
"g1nation.memoryShortTermMessages": {
"type": "number",
"default": 8,
"minimum": 0,
"description": "Number of recent conversation messages included as short-term memory."
},
"g1nation.memoryMediumTermSessions": {
"type": "number",
"default": 5,
"minimum": 0,
"description": "Number of recent saved chat sessions included as medium-term memory."
},
"g1nation.memoryLongTermFiles": {
"type": "number",
"default": 6,
"minimum": 0,
"description": "Number of relevant Second Brain markdown files included as long-term memory."
},
"g1nation.ollamaUrl": {
"type": "string",
"default": "http://127.0.0.1:11434",
+102 -5
View File
@@ -298,12 +298,18 @@ export class AgentExecutor {
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
: '';
const agentSkillCtx = options.agentSkillContext ? `\n\n[AGENT PERSONA & SKILLS]\n${options.agentSkillContext}` : '';
const selectedAgentSystemPrompt = !multiAgentEnabled && options.agentSkillContext
? `\n\n[SELECTED AGENT MODE]\nThe user selected the following Agent skill. Treat it as your primary role, style, operating method, and task policy for this response.\n${options.agentSkillContext}`
: '';
const agentSkillCtx = multiAgentEnabled && options.agentSkillContext
? `\n\n[SELECTED AGENT REFERENCE]\nUse this selected Agent skill as additional preference context when it is relevant.\n${options.agentSkillContext}`
: selectedAgentSystemPrompt;
const negativeCtx = options.negativePrompt
? `\n\n### CRITICAL NEGATIVE CONSTRAINTS (DO NOT DO THESE)\n${options.negativePrompt}\n\n[SYSTEM_RULE: Apply the above constraints strictly. DO NOT mention or repeat these constraints in your response.]`
: '';
const memoryCtx = this.buildMemoryContext(prompt || '');
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${agentSkillCtx}${negativeCtx}`;
const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${negativeCtx}`;
const messagesForRequest: ChatMessage[] = [
{ role: 'system', content: fullSystemPrompt, internal: true },
...reqMessages
@@ -558,17 +564,23 @@ export class AgentExecutor {
logError('Failed to load brain context for agents', ctxErr);
}
// 워크플로우 매니저에게 실행 위임 (Strict Synchronization & Contract)
const workflow = getConfig().multiAgentWorkflow;
const selectedAgentContext = options.agentSkillContext
? `\nSelected Agent Reference:\n${options.agentSkillContext}`
: '';
// 워크플로우 매니저에게 설정 기반 실행 위임
const finalReport = await AgentWorkflowManager.runStrictWorkflow(
prompt,
modelName,
brainContext,
`${brainContext}${selectedAgentContext}`,
signal,
(step, msg) => {
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
// 각 단계별 시작을 알림
this.webview?.postMessage({ type: 'streamChunk', value: `\n\n> **[${step}]** ${msg}\n\n` });
}
},
workflow
);
if (signal.aborted || !this.webview) return;
@@ -917,6 +929,91 @@ export class AgentExecutor {
return runId !== this.activeRunId;
}
private buildMemoryContext(currentPrompt: string): string {
const config = getConfig();
if (!config.memoryEnabled) return '';
const visibleHistory = this.chatHistory.filter((message) => !message.internal);
const shortTerm = visibleHistory
.slice(-config.memoryShortTermMessages)
.map((message) => `- ${message.role}: ${summarizeText(message.content, 260)}`)
.join('\n');
const savedSessions = this.context.globalState.get<any[]>('chat_sessions', []) || [];
const mediumTerm = savedSessions
.slice(0, config.memoryMediumTermSessions)
.map((session: any) => {
const title = summarizeText(String(session?.title || 'Untitled session'), 120);
const lastMessage = Array.isArray(session?.history)
? session.history[session.history.length - 1]?.content || ''
: '';
return `- ${title}: ${summarizeText(String(lastMessage), 220)}`;
})
.join('\n');
const longTerm = this.findRelevantBrainMemory(currentPrompt, config.memoryLongTermFiles);
const sections = [
shortTerm ? `### Short-Term Memory\n${shortTerm}` : '',
mediumTerm ? `### Medium-Term Memory\n${mediumTerm}` : '',
longTerm ? `### Long-Term Memory\n${longTerm}` : ''
].filter(Boolean).join('\n\n');
if (!sections) return '';
return [
'',
'[MEMORY CONTEXT]',
'Review this layered memory before preparing the answer. Use it only when relevant, and prefer the current user request when there is conflict.',
sections
].join('\n');
}
private findRelevantBrainMemory(currentPrompt: string, limit: number): string {
if (limit <= 0) return '';
try {
const activeBrain = getActiveBrainProfile();
const files = findBrainFiles(activeBrain.localBrainPath);
const terms = currentPrompt
.toLowerCase()
.split(/[^a-z0-9가-힣_]+/g)
.filter((term) => term.length >= 2)
.slice(0, 24);
const scored = files.map((file) => {
let score = 0;
const basename = path.basename(file).toLowerCase();
for (const term of terms) {
if (basename.includes(term)) score += 4;
}
let preview = '';
try {
const content = fs.readFileSync(file, 'utf8');
const lower = content.toLowerCase();
for (const term of terms) {
if (lower.includes(term)) score += 1;
}
preview = summarizeText(content, 360);
} catch {
preview = '';
}
const stat = fs.existsSync(file) ? fs.statSync(file) : undefined;
return { file, score, preview, mtime: stat?.mtimeMs || 0 };
});
return scored
.sort((a, b) => (b.score - a.score) || (b.mtime - a.mtime))
.slice(0, limit)
.map((entry) => `- ${path.relative(activeBrain.localBrainPath, entry.file)}: ${entry.preview}`)
.join('\n');
} catch (error: any) {
logError('Failed to build long-term memory context.', { error: error?.message || String(error) });
return '';
}
}
private emitHistoryChanged() {
if (!this.historyChangeListener) return;
+45 -41
View File
@@ -1,6 +1,5 @@
import * as vscode from 'vscode';
import { PlannerAgent, ResearcherAgent, WriterAgent } from './factory';
import { AgentEngine, PipelineStage } from '../lib/engine';
import { ConfigurableWorkflowAgent } from './factory';
import { DEFAULT_MULTI_AGENT_WORKFLOW, MultiAgentStageConfig } from '../config';
export class AgentWorkflowManager {
/**
@@ -11,33 +10,28 @@ export class AgentWorkflowManager {
modelName: string,
brainContext: string,
signal: AbortSignal,
onProgress: (step: string, message: string) => void
onProgress: (step: string, message: string) => void,
workflow: MultiAgentStageConfig[] = DEFAULT_MULTI_AGENT_WORKFLOW
): Promise<string> {
// 1. 에이전트 준비 (DI를 위한 인스턴스화)
const planner = new PlannerAgent(modelName);
const researcher = new ResearcherAgent(modelName);
const writer = new WriterAgent(modelName);
// 2. 엔진 인스턴스 생성 (의존성 주입)
const engine = new AgentEngine(planner, researcher, writer);
// 3. 고유 미션 ID 생성 (현재는 타임스탬프 기반)
const missionId = `mission_${Date.now()}`;
const stages = workflow.length > 0 ? workflow : DEFAULT_MULTI_AGENT_WORKFLOW;
const stageOutputs: string[] = [];
try {
// 4. 엔진을 통한 미션 실행 (Producer-Consumer & Mutex 적용)
return await engine.runMission(
missionId,
prompt,
brainContext,
signal,
(stage: PipelineStage, message: string) => {
// UI 피드백을 위한 프로그레스 업데이트
const uiStepName = this.mapStageToUI(stage);
onProgress(uiStepName, message);
}
);
for (const [index, stage] of stages.entries()) {
if (signal.aborted) throw new Error('AbortError');
const agent = new ConfigurableWorkflowAgent(modelName, stage);
const stepName = stage.name || `Stage ${index + 1}`;
onProgress(stepName, `Running ${index + 1}/${stages.length}`);
const stageInput = this.buildStageInput(stage, prompt, stageOutputs);
const result = await agent.execute(stageInput, brainContext, signal);
this.validateResult(result, stepName);
stageOutputs.push(result);
onProgress(stepName, `Completed ${index + 1}/${stages.length}`);
}
return stageOutputs[stageOutputs.length - 1];
} catch (error: any) {
if (error.name === 'AbortError' || error.message.includes('cancelled')) {
throw error;
@@ -46,18 +40,28 @@ export class AgentWorkflowManager {
}
}
/**
* 엔진 스테이지를 UI 표시용 명칭으로 매핑
*/
private static mapStageToUI(stage: PipelineStage): string {
const maps: Record<PipelineStage, string> = {
idle: '대기',
planner: 'Planner',
researcher: 'Researcher',
writer: 'Writer',
completed: '완료',
error: '오류'
};
return maps[stage] || '진행 중';
private static buildStageInput(stage: MultiAgentStageConfig, originalPrompt: string, stageOutputs: string[]): string {
const previous = stageOutputs[stageOutputs.length - 1] || '';
if (stage.input === 'original') {
return originalPrompt;
}
if (stage.input === 'previous') {
return previous || originalPrompt;
}
return [
'## Original User Request',
originalPrompt,
'',
'## Previous Stage Output',
previous || 'No previous stage output.'
].join('\n');
}
private static validateResult(data: string, step: string) {
if (!data || data.trim().length < 10) {
throw new Error(`${step} agent did not return a usable response.`);
}
}
}
+27 -1
View File
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { getConfig } from '../config';
import { MultiAgentStageConfig, getConfig } from '../config';
export abstract class BaseAgent {
constructor(protected readonly modelName: string) {}
@@ -145,3 +145,29 @@ Your goal is to produce a state-of-the-art final report that wows the user.
return this.callLLM(this.persona, wrappedInput, signal);
}
}
export class ConfigurableWorkflowAgent extends BaseAgent {
constructor(
modelName: string,
private readonly stage: MultiAgentStageConfig
) {
super(modelName);
}
async execute(input: string, context?: string, signal?: AbortSignal): Promise<string> {
const wrappedInput = [
`### Stage: ${this.stage.name}`,
'',
'### Available Context',
context || 'No specific context available.',
'',
'### Stage Input',
input,
'',
'### Mission',
'Complete this stage according to your role instructions. Be concise, concrete, and preserve details needed by later stages.'
].join('\n');
return this.callLLM(this.stage.prompt, wrappedInput, signal);
}
}
+73 -2
View File
@@ -12,6 +12,15 @@ export interface BrainProfile {
description?: string;
}
export type MultiAgentStageInput = 'original' | 'previous' | 'combined';
export interface MultiAgentStageConfig {
id: string;
name: string;
prompt: string;
input?: MultiAgentStageInput;
}
// ─── 에이전트 설정 인터페이스 (통합 버전) ───
export interface IAgentConfig {
ollamaUrl: string;
@@ -26,8 +35,46 @@ export interface IAgentConfig {
maxAutoSteps: number;
dryRun: boolean;
multiAgentEnabled: boolean;
multiAgentWorkflow: MultiAgentStageConfig[];
memoryEnabled: boolean;
memoryShortTermMessages: number;
memoryMediumTermSessions: number;
memoryLongTermFiles: number;
}
export const DEFAULT_MULTI_AGENT_WORKFLOW: MultiAgentStageConfig[] = [
{
id: 'planner',
name: 'Planner',
input: 'original',
prompt: [
'You are the [Master Strategist & Planner].',
'Transform the user request into a precise execution blueprint.',
'Output Markdown with Objective, Core Challenges, Data Requirements, and Step-by-Step Tasks.'
].join('\n')
},
{
id: 'researcher',
name: 'Researcher',
input: 'combined',
prompt: [
'You are the [Senior Technical Researcher].',
'Extract, filter, and synthesize high-signal facts from the plan and available context.',
'Output Key Facts, Technical Deep-Dive, Gaps, and Summary of Knowledge.'
].join('\n')
},
{
id: 'writer',
name: 'Writer',
input: 'combined',
prompt: [
'You are the [Lead Synthesis Writer & Editor].',
'Produce the final response in the user language with a clear conclusion and actionable recommendation.',
'Preserve important technical nuance without padding.'
].join('\n')
}
];
// ─── 경로 정규화 유틸리티 ───
function normalizePath(p: string): string {
if (!p) return p;
@@ -50,6 +97,21 @@ function toBrainProfile(raw: Partial<BrainProfile> | undefined, fallbackIndex: n
};
}
function toMultiAgentStage(raw: Partial<MultiAgentStageConfig> | undefined, fallbackIndex: number): MultiAgentStageConfig | null {
if (!raw) return null;
const prompt = typeof raw.prompt === 'string' ? raw.prompt.trim() : '';
if (!prompt) return null;
const input = raw.input === 'original' || raw.input === 'previous' || raw.input === 'combined'
? raw.input
: 'combined';
return {
id: (raw.id || `stage-${fallbackIndex + 1}`).trim(),
name: (raw.name || `Stage ${fallbackIndex + 1}`).trim(),
prompt,
input
};
}
// ─── VS Code 설정에서 읽어오는 값 (통합 구현) ───
export function getConfig(): IAgentConfig {
const cfg = vscode.workspace.getConfiguration('g1nation');
@@ -58,9 +120,13 @@ export function getConfig(): IAgentConfig {
const legacyBrainPath = cfg.get<string>('localBrainPath', '');
const legacyBrainRepo = cfg.get<string>('secondBrainRepo', '');
const configuredProfiles = cfg.get<Partial<BrainProfile>[]>('brainProfiles', []);
const configuredWorkflow = cfg.get<Partial<MultiAgentStageConfig>[]>('multiAgentWorkflow', DEFAULT_MULTI_AGENT_WORKFLOW);
const profiles = configuredProfiles
.map((profile, index) => toBrainProfile(profile, index))
.filter((profile): profile is BrainProfile => !!profile);
const multiAgentWorkflow = configuredWorkflow
.map((stage, index) => toMultiAgentStage(stage, index))
.filter((stage): stage is MultiAgentStageConfig => !!stage);
if (profiles.length === 0) {
const fallbackPath = normalizePath(legacyBrainPath) || path.join(os.homedir(), '.g1nation-brain');
@@ -99,7 +165,12 @@ export function getConfig(): IAgentConfig {
maxContextSize: cfg.get<number>('maxContextSize', 12000),
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50),
dryRun: cfg.get<boolean>('dryRun', false),
multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', true)
multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', true),
multiAgentWorkflow: multiAgentWorkflow.length > 0 ? multiAgentWorkflow : DEFAULT_MULTI_AGENT_WORKFLOW,
memoryEnabled: cfg.get<boolean>('memoryEnabled', true),
memoryShortTermMessages: Math.max(0, cfg.get<number>('memoryShortTermMessages', 8)),
memoryMediumTermSessions: Math.max(0, cfg.get<number>('memoryMediumTermSessions', 5)),
memoryLongTermFiles: Math.max(0, cfg.get<number>('memoryLongTermFiles', 6))
};
}
@@ -154,4 +225,4 @@ export const EXCLUDED_DIRS = new Set([
'node_modules', '.git', '.vscode', 'out', 'dist', 'build',
'.next', '.cache', '__pycache__', '.DS_Store', 'coverage',
'.turbo', '.nuxt', '.output', 'vendor', 'target'
]);
]);
+60 -37
View File
@@ -1198,7 +1198,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px 8px;
padding: 10px 12px 6px;
gap: 10px;
}
@@ -1210,7 +1210,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
.header-actions,
.tool-strip,
.tool-group,
.select-stack,
.select-line,
@@ -1219,8 +1218,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
align-items: center;
}
.header-actions { gap: 6px; flex-shrink: 0; }
.tool-strip { gap: 6px; flex-wrap: wrap; }
.header-actions { gap: 6px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
.tool-group {
gap: 4px;
padding: 3px;
@@ -1230,6 +1228,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
.select-stack { flex-direction: column; gap: 6px; min-width: 0; }
.select-line { gap: 6px; width: 100%; min-width: 0; }
.paired-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
width: 100%;
align-items: center;
}
.control-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1237,12 +1242,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
width: 100%;
align-items: center;
}
.utility-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.brand { font-weight: 700; font-size: 14px; color: var(--text-bright); letter-spacing: 0; display: flex; align-items: center; gap: 8px; min-width: 0; }
.logo { width: 22px; height: 22px; background: var(--accent); color: #fff; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 900; }
@@ -1289,9 +1288,18 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
animation: msgIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.msg-user {
align-items: flex-end;
}
.msg-ai {
align-items: flex-start;
}
@keyframes msgIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.msg-head { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 11px; color: var(--text-dim); }
.msg-user .msg-head { flex-direction: row-reverse; }
.av { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; }
/* Tooltip System */
@@ -1309,10 +1317,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
.msg-body {
padding-left: 30px;
color: var(--text-primary);
font-size: 13.5px;
word-break: break-word;
max-width: min(88%, 760px);
}
.msg-user .msg-body {
@@ -1320,8 +1328,14 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px 14px;
margin-left: 30px;
white-space: pre-wrap;
text-align: left;
}
.msg-ai .msg-body {
padding-left: 30px;
width: 100%;
max-width: 100%;
}
/* --- Markdown Style --- */
@@ -1367,8 +1381,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
background: var(--control-bg);
border: 1px solid var(--border);
color: var(--text-dim);
width: 28px;
min-width: 28px;
height: 28px;
padding: 0 8px;
border-radius: 6px;
display: inline-flex;
align-items: center;
@@ -1378,7 +1393,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
font-size: 12px;
font-weight: 700;
line-height: 1;
flex: 0 0 28px;
flex: 0 0 auto;
}
.icon-btn:hover { color: var(--text-bright); border-color: var(--border-bright); background: var(--control-bg-hover); box-shadow: var(--shadow-soft); }
.icon-btn.active { color: var(--accent); border-color: var(--accent); background: var(--control-active-bg); }
@@ -1724,10 +1739,18 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
@media (min-width: 360px) {
.header-controls {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr);
align-items: start;
}
}
@media (max-width: 520px) {
.paired-row {
grid-template-columns: minmax(0, 1fr);
}
.header-top {
align-items: flex-start;
}
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
@@ -1739,8 +1762,12 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
<div class="header-top">
<div class="brand"><div class="logo">✦</div> G1nation</div>
<div class="header-actions">
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">+</button>
<button class="icon-btn" id="settingsBtn" data-tooltip="Settings">⚙</button>
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">New</button>
<button class="icon-btn" id="saveWikiRawBtn" data-tooltip="Save Wiki Raw">Wiki</button>
<button class="icon-btn" id="multiAgentBtn" data-tooltip="Multi-Agent Mode">MA</button>
<button class="icon-btn" id="internetBtn" data-tooltip="Internet Access">Web</button>
<button class="icon-btn" id="historyBtn" data-tooltip="View History">Log</button>
<button class="icon-btn" id="settingsBtn" data-tooltip="Settings">Set</button>
</div>
</div>
<div class="header-controls">
@@ -1749,29 +1776,25 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
<div class="status-pill"><span id="statusDot" class="status-dot"></span><span id="engineStatusText">Engine</span></div>
<div class="select-wrap"><select id="modelSel" title="Select Model"></select></div>
</div>
<div class="control-row">
<div class="select-wrap"><select id="brainSel" title="Select Brain"></select></div>
<div class="tool-group" aria-label="Brain actions">
<button class="icon-btn" id="addBrainBtn" data-tooltip="Add Brain">+</button>
<button class="icon-btn" id="editBrainBtn" data-tooltip="Edit Brain"></button>
<button class="icon-btn" id="deleteBrainBtn" data-tooltip="Delete Brain"></button>
<button class="icon-btn" id="brainBtn" data-tooltip="Sync Knowledge">↻</button>
<div class="paired-row">
<div class="control-row">
<div class="select-wrap"><select id="brainSel" title="Select Brain"></select></div>
<div class="tool-group" aria-label="Brain actions">
<button class="icon-btn" id="addBrainBtn" data-tooltip="Add Brain">Add</button>
<button class="icon-btn" id="editBrainBtn" data-tooltip="Edit Brain">Edit</button>
<button class="icon-btn" id="deleteBrainBtn" data-tooltip="Delete Brain">Del</button>
<button class="icon-btn" id="brainBtn" data-tooltip="Sync Knowledge">Sync</button>
</div>
</div>
</div>
<div class="control-row">
<div class="select-wrap"><select id="agentSel" title="Select Agentic Skill"></select></div>
<div class="tool-group" aria-label="Agent actions">
<button class="icon-btn" id="addAgentBtn" data-tooltip="Create Agent">+</button>
<button class="icon-btn" id="editAgentBtn" data-tooltip="Edit Agent Skill"></button>
<button class="icon-btn" id="deleteAgentBtn" data-tooltip="Delete Agent Skill"></button>
<div class="control-row">
<div class="select-wrap"><select id="agentSel" title="Select Agentic Skill"></select></div>
<div class="tool-group" aria-label="Agent actions">
<button class="icon-btn" id="addAgentBtn" data-tooltip="Create Agent">Add</button>
<button class="icon-btn" id="editAgentBtn" data-tooltip="Edit Agent Skill">Edit</button>
<button class="icon-btn" id="deleteAgentBtn" data-tooltip="Delete Agent Skill">Del</button>
</div>
</div>
</div>
<div class="utility-row">
<button class="icon-btn" id="saveWikiRawBtn" data-tooltip="Save Wiki Raw">□</button>
<button class="icon-btn" id="multiAgentBtn" data-tooltip="Multi-Agent Mode">⧉</button>
<button class="icon-btn" id="internetBtn" data-tooltip="Internet Access">↗</button>
<button class="icon-btn" id="historyBtn" data-tooltip="View History">◷</button>
</div>
</div>
</div>
</div>