feat: v2.12.0 - UI/UX Refinement (Model Sync & Premium Tooltips)

This commit is contained in:
Wonseok Jung
2026-04-30 00:19:06 +09:00
parent f8a57cfbb0
commit 326672cb93
25 changed files with 5606 additions and 363 deletions
+381 -15
View File
@@ -2,7 +2,6 @@ import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import {
getConfig,
_getBrainDir,
findBrainFiles,
buildApiUrl,
@@ -13,6 +12,7 @@ import {
resolveEngine,
summarizeText
} from './utils';
import { getConfig } from './config';
import { AgentExecutor, ChatMessage } from './agent';
import { BridgeInterface } from './bridge';
@@ -88,8 +88,12 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await this._sendBrainProfiles();
await this._sendSessionList();
await this._sendModels();
await this._sendConfig();
await this._restoreActiveSessionIntoView();
break;
case 'toggleMultiAgent':
await vscode.workspace.getConfiguration('g1nation').update('multiAgentEnabled', data.value, vscode.ConfigurationTarget.Global);
break;
case 'getModels':
await this._sendModels();
break;
@@ -128,6 +132,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await this.syncBrain();
await this._sendBrainStatus();
break;
case 'addMessage':
this._view?.webview.postMessage({ type: 'addMessage', role: data.role, value: data.value, rationale: data.rationale });
break;
case 'addBrain':
await this._addBrainProfile();
break;
@@ -143,6 +150,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
case 'refreshModels':
await this._sendModels();
break;
case 'model':
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', data.value, vscode.ConfigurationTarget.Global);
logInfo(`Default model updated to: ${data.value}`);
break;
case 'proactiveTrigger':
await this._handleProactiveSuggestion(data.context);
break;
case 'exportResponse':
const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath || '';
const defaultPath = path.join(workspacePath, 'g1_response.md');
@@ -155,6 +169,12 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
vscode.window.showInformationMessage(`✅ Exported to ${path.basename(uri.fsPath)}`);
}
break;
case 'approveAction':
await this._agent.approveTransaction();
break;
case 'rejectAction':
await this._agent.rejectTransaction();
break;
}
});
}
@@ -378,6 +398,17 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await this._context.globalState.update('chat_sessions', sessions.slice(0, 50));
}
private async _sendConfig() {
if (!this._view) return;
const config = getConfig();
this._view.webview.postMessage({
type: 'configUpdate',
value: {
multiAgentEnabled: config.multiAgentEnabled
}
});
}
private async _sendBrainStatus() {
if (!this._view) return;
const activeBrain = getActiveBrainProfile();
@@ -631,6 +662,29 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
this._view.webview.postMessage({ type: 'agentsList', value: agents, selected: lastPath });
}
private async _handleProactiveSuggestion(context: string) {
if (!this._view) return;
let suggestion = '';
switch (context) {
case 'settings_exploration':
suggestion = '💡 **Tip:** 모델 설정을 최적화하여 답변 속도를 2배 이상 높일 수 있습니다. 설정에서 `Max Context Size`를 조정해보세요!';
break;
case 'brain_sync_exploration':
suggestion = '🧠 **Knowledge Sync:** 최근에 수정한 파일이 지식 베이스에 반영되지 않았나요? 지금 동기화 버튼을 눌러 최신 정보를 업데이트하세요.';
break;
case 'agent_selection_exploration':
suggestion = '🤖 **Agent Skills:** 특정 언어나 프레임워크에 특화된 에이전트 스킬을 선택하면 더 정확한 코드를 생성할 수 있습니다.';
break;
default:
suggestion = '💡 새로운 기능을 발견하셨나요? 궁금한 점이 있다면 언제든 물어보세요!';
}
this._view.webview.postMessage({ type: 'streamStart' });
this._view.webview.postMessage({ type: 'streamChunk', value: `\n\n> [!TIP]\n> ${suggestion}\n` });
this._view.webview.postMessage({ type: 'streamEnd' });
}
private async _createAgent() {
const name = await vscode.window.showInputBox({
prompt: 'Name of the new Agent (e.g., frontend_expert)',
@@ -891,8 +945,24 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
.msg-head { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 11px; color: var(--text-dim); }
.av { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; }
.av-user { background: var(--border); color: var(--text-primary); }
.av-ai { background: var(--accent); color: #fff; }
.icon-btn:hover { background: var(--border); color: var(--text-bright); }
.icon-btn.active { color: var(--accent); border-color: var(--accent); background: rgba(187, 134, 252, 0.1); }
/* Tooltip System */
[data-tooltip] { position: relative; }
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute; bottom: -30px; left: 50%; transform: translateX(-50%);
background: #333; color: #fff; padding: 4px 8px; border-radius: 4px;
font-size: 10px; white-space: nowrap; opacity: 0; pointer-events: none;
transition: all 0.2s ease; z-index: 100; box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
[data-tooltip]:hover::after { opacity: 1; bottom: -35px; }
.header-controls { display: flex; gap: 8px; margin-left: auto; }
#promptInput::placeholder { color: var(--accent); opacity: 0.6; font-weight: 500; }
.msg-body {
padding-left: 30px;
@@ -997,11 +1067,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
/* --- Overlays & Others --- */
.thinking-bar { height: 2px; background: transparent; position: relative; overflow: hidden; margin-top: -1px; }
.thinking-bar.active::after {
content: ''; position: absolute; top: 0; left: -40%; width: 40%; height: 100%;
background: linear-gradient(90deg, transparent, var(--accent), transparent); animation: think 1.5s infinite;
.thinking-bar.active {
display: block;
background: linear-gradient(90deg, transparent, #2196f3, #bb86fc, transparent);
background-size: 200% 100%;
animation: thinking 1.5s infinite linear;
}
@keyframes think { 0% { left: -40%; } 100% { left: 100%; } }
@keyframes thinking { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.history-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8);
@@ -1025,6 +1097,140 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
margin-bottom: 10px; cursor: pointer; transition: 0.2s;
}
.history-item:hover { border-color: var(--accent); background: var(--accent-glow); }
/* --- Approval UI --- */
.approval-box {
display: flex;
flex-direction: column;
gap: 12px;
margin: 15px 0;
padding: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-bright);
border-radius: 12px;
animation: msgIn 0.3s ease-out;
}
.approval-title { font-weight: 700; color: var(--accent); font-size: 12px; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
.approval-btns { display: flex; gap: 10px; }
.btn-approve { flex: 1; background: var(--success); color: white; border: none; padding: 10px; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 12px; transition: 0.2s; }
.btn-approve:hover { filter: brightness(1.1); transform: translateY(-1px); }
.btn-reject { flex: 1; background: var(--error); color: white; border: none; padding: 10px; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 12px; transition: 0.2s; }
.btn-reject:hover { filter: brightness(1.1); transform: translateY(-1px); }
/* --- Physics & Micro-interactions --- */
button {
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
outline: none;
}
button:active {
transform: scale(0.96);
filter: brightness(0.9);
}
/* --- Hierarchical Grouping --- */
.input-group {
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
padding: 8px;
margin-top: 10px;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
gap: 8px;
}
/* --- Storytelling Stepper --- */
.stepper-container {
display: none;
margin: 12px 16px;
padding: 12px;
background: rgba(var(--accent-rgb), 0.05);
border-radius: 8px;
border: 1px solid rgba(var(--accent-rgb), 0.1);
animation: slideIn 0.3s ease-out;
}
.stepper-container.active { display: block; }
.steps {
display: flex;
justify-content: space-between;
position: relative;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
flex: 1;
}
.step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-dim);
transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.step.active .step-dot {
background: var(--accent);
box-shadow: 0 0 12px var(--accent);
transform: scale(1.5);
}
.step.complete .step-dot {
background: var(--success);
box-shadow: 0 0 8px var(--success);
}
.step-label {
font-size: 9px;
color: var(--text-dim);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.step.active .step-label { color: var(--accent); }
.step.complete .step-label { color: var(--success); }
/* --- Rationale View (Thought Process) --- */
.rationale-container {
margin: 12px 0;
padding: 16px;
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid var(--accent);
border-radius: 4px 12px 12px 4px;
font-size: 12px;
line-height: 1.5;
animation: slideIn 0.3s ease-out;
backdrop-filter: blur(10px);
}
.rationale-header {
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--accent);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.rationale-section {
margin-bottom: 10px;
}
.rationale-label {
display: flex;
align-items: center;
gap: 6px;
font-weight: 700;
color: var(--text-bright);
margin-bottom: 4px;
font-size: 11px;
}
.rationale-content {
color: var(--text-dim);
padding-left: 20px;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
</style>
</head>
<body>
@@ -1043,9 +1249,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
<select id="agentSel" title="Select Agentic Skill"></select>
<button class="icon-btn" id="editAgentBtn" title="Edit Agent Skill">📝</button>
<button class="icon-btn" id="addAgentBtn" title="Create Agent">+</button>
<button class="icon-btn" id="internetBtn" title="Internet Access">🌐</button>
<button class="icon-btn" id="brainBtn" title="Sync Knowledge">🧠</button>
<button class="icon-btn" id="historyBtn" title="History">📜</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="brainBtn" data-tooltip="Sync Knowledge">🧠</button>
<button class="icon-btn" id="historyBtn" data-tooltip="View History">📜</button>
</div>
</div>
@@ -1059,6 +1266,15 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
<div class="thinking-bar" id="thinkingBar"></div>
<div id="stepper" class="stepper-container">
<div class="steps">
<div class="step" id="step-analyze"><div class="step-dot"></div><div class="step-label">Analyze</div></div>
<div class="step" id="step-plan"><div class="step-dot"></div><div class="step-label">Plan</div></div>
<div class="step" id="step-execute"><div class="step-dot"></div><div class="step-label">Execute</div></div>
<div class="step" id="step-verify"><div class="step-dot"></div><div class="step-label">Verify</div></div>
</div>
</div>
<div class="chat" id="chat">
<div class="welcome">
<div class="welcome-logo">✦</div>
@@ -1088,6 +1304,11 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
<button id="sendBtn" class="send-btn">Send</button>
</div>
</div>
<div class="input-group">
<button class="action-btn" style="flex:1" id="inputNewChatBtn">New Chat</button>
<button class="action-btn" style="flex:1" id="inputSyncBtn">Sync Knowledge</button>
</div>
</div>
<input type="file" id="fileInput" multiple hidden accept="image/*,.txt,.md,.pdf,.csv,.json,.js,.ts,.py,.java,.rs,.go">
</div>
@@ -1098,6 +1319,52 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const sendBtn = document.getElementById('sendBtn');
const thinkingBar = document.getElementById('thinkingBar');
const statusLabel = document.getElementById('statusLabel');
const stepper = document.getElementById('stepper');
// --- Sound Manager ---
const Sound = {
ctx: null,
init() { if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
play(freq, type, dur) {
try {
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + dur);
} catch(e) {}
},
success() { this.play(880, 'sine', 0.1); setTimeout(() => this.play(1109, 'sine', 0.15), 80); },
warn() { this.play(440, 'triangle', 0.3); }
};
function setStep(stepId, state = 'active') {
stepper.classList.add('active');
const step = document.getElementById('step-' + stepId);
if (step) {
if (state === 'active') {
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
step.classList.add('active');
} else if (state === 'complete') {
step.classList.remove('active');
step.classList.add('complete');
}
}
}
function resetStepper() {
stepper.classList.remove('active');
document.querySelectorAll('.step').forEach(s => {
s.classList.remove('active');
s.classList.remove('complete');
});
}
const modelSel = document.getElementById('modelSel');
const brainSel = document.getElementById('brainSel');
const historyOverlay = document.getElementById('historyOverlay');
@@ -1133,16 +1400,45 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
document.body.removeChild(textarea);
}
window.approve = () => {
const box = document.querySelector('.approval-box');
if (box) box.remove();
vscode.postMessage({ type: 'approveAction' });
};
window.reject = () => {
const box = document.querySelector('.approval-box');
if (box) box.remove();
vscode.postMessage({ type: 'rejectAction' });
};
function exportToMD(text) {
vscode.postMessage({ type: 'exportResponse', text: text });
}
function addMsg(text, role) {
function addMsg(text, role, rationale) {
const isUser = role === 'user';
const msgEl = document.createElement('div');
msgEl.className = 'msg ' + (isUser ? 'msg-user' : 'msg-ai');
msgEl._raw = text;
// If rationale exists and it's an AI message, add the Rationale View
if (!isUser && rationale && (rationale.problem || rationale.goal || rationale.reasoning)) {
const ratDiv = document.createElement('div');
ratDiv.className = 'rationale-container';
let ratHtml = '<div class="rationale-header"><span>🧠</span> Thought Process</div>';
if (rationale.problem) {
ratHtml += '<div class="rationale-section"><div class="rationale-label"><span>⚠️</span> Problem</div><div class="rationale-content">' + rationale.problem + '</div></div>';
}
if (rationale.goal) {
ratHtml += '<div class="rationale-section"><div class="rationale-label"><span>💡</span> Goal</div><div class="rationale-content">' + rationale.goal + '</div></div>';
}
if (rationale.reasoning) {
ratHtml += '<div class="rationale-section"><div class="rationale-label"><span>✅</span> Rationale</div><div class="rationale-content">' + rationale.reasoning + '</div></div>';
}
ratDiv.innerHTML = ratHtml;
chat.appendChild(ratDiv);
}
const head = document.createElement('div');
head.className = 'msg-head';
head.innerHTML = isUser ? '<div class="av av-user">U</div> You' : '<div class="av av-ai">✦</div> G1nation';
@@ -1179,6 +1475,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
window.addEventListener('message', e => {
const msg = e.data;
switch(msg.type) {
case 'addMessage':
addMsg(msg.value, msg.role, msg.rationale);
break;
case 'streamStart':
thinkingBar.classList.remove('active');
if (document.querySelector('.welcome')) document.querySelector('.welcome').remove();
@@ -1196,6 +1495,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
case 'streamEnd':
if (streamBody) streamBody.classList.remove('stream-active');
streamBody = null; sendBtn.disabled = false;
resetStepper();
Sound.success();
break;
case 'restoreHistory':
case 'sessionLoaded':
@@ -1204,7 +1505,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (history && history.length > 0) {
chat.innerHTML = '';
history.forEach(m => addMsg(m.content, m.role === 'assistant' ? 'assistant' : 'user'));
history.forEach(m => addMsg(m.content, m.role === 'assistant' ? 'assistant' : 'user', m.rationale));
}
if (historyData.negativePrompt !== undefined) {
negativePrompt.value = historyData.negativePrompt;
@@ -1224,6 +1525,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (m === msg.value.selected) o.selected = true;
modelSel.appendChild(o);
});
if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder();
statusLabel.innerText = \`Model: \${msg.value.selected}\`;
break;
case 'brainProfiles':
@@ -1251,6 +1553,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
break;
case 'autoContinue':
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
if (msg.value.includes('Analyzing')) setStep('analyze');
if (msg.value.includes('Planning')) setStep('plan');
if (msg.value.includes('Executing')) setStep('execute');
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
break;
case 'agentsList':
@@ -1272,6 +1577,18 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
thinkingBar.classList.remove('active'); sendBtn.disabled = false;
addMsg(msg.value, 'error');
break;
case 'requiresApproval':
const box = document.createElement('div');
box.className = 'approval-box';
box.innerHTML = '<div class="approval-title"><span>🛡️</span> 작업 승인 대기 중 (Action Approval Required)</div>' +
'<div style="font-size: 11px; color: var(--text-dim); margin-bottom: 8px;">위의 변경 사항을 프로젝트에 반영할까요?</div>' +
'<div class="approval-btns">' +
' <button class="btn-approve" onclick="approve()">승인 (Approve)</button>' +
' <button class="btn-reject" onclick="reject()">롤백 (Rollback)</button>' +
'</div>';
chat.appendChild(box);
chat.scrollTop = chat.scrollHeight;
break;
}
});
@@ -1327,16 +1644,36 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
});
input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; });
document.getElementById('newChatBtn').onclick = () => vscode.postMessage({ type: 'newChat' });
const startNewChat = () => { Sound.play(660, 'sine', 0.1); vscode.postMessage({ type: 'newChat' }); };
document.getElementById('newChatBtn').onclick = startNewChat;
document.getElementById('inputNewChatBtn').onclick = startNewChat;
document.getElementById('settingsBtn').onclick = () => vscode.postMessage({ type: 'openSettings' });
document.getElementById('internetBtn').onclick = () => {
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
};
document.getElementById('brainBtn').onclick = () => vscode.postMessage({ type: 'syncBrain' });
let multiAgentEnabled = true;
document.getElementById('multiAgentBtn').onclick = () => {
multiAgentEnabled = !multiAgentEnabled;
vscode.postMessage({ type: 'toggleMultiAgent', value: multiAgentEnabled });
document.getElementById('multiAgentBtn').classList.toggle('active', multiAgentEnabled);
};
const syncBrain = () => { Sound.play(550, 'sine', 0.1); vscode.postMessage({ type: 'syncBrain' }); };
document.getElementById('brainBtn').onclick = syncBrain;
document.getElementById('inputSyncBtn').onclick = syncBrain;
document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' });
document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible'));
document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible');
modelSel.onchange = () => vscode.postMessage({ type: 'refreshModels' });
const updateInputPlaceholder = () => {
promptInput.placeholder = \`Ask \${modelSel.value}...\`;
};
modelSel.onchange = () => {
vscode.postMessage({ type: 'model', value: modelSel.value });
updateInputPlaceholder();
};
brainSel.onchange = () => {
if (brainSel.value === 'new') {
vscode.postMessage({ type: 'addBrain' });
@@ -1345,6 +1682,16 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
};
// Handle initial state and state updates from extension
window.addEventListener('message', e => {
const msg = e.data;
if (msg.type === 'configUpdate') {
multiAgentEnabled = msg.value.multiAgentEnabled;
document.getElementById('multiAgentBtn').classList.toggle('active', multiAgentEnabled);
}
});
agentSel.onchange = () => {
if (agentSel.value !== 'none') {
vscode.postMessage({ type: 'getAgentContent', path: agentSel.value });
@@ -1381,6 +1728,25 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
vscode.postMessage({ type: 'getModels' });
vscode.postMessage({ type: 'getAgents' });
vscode.postMessage({ type: 'ready' });
// --- Proactive Behavioral Tracking ---
let hoverTimer = null;
const trackBehavior = (elementId, context) => {
const el = document.getElementById(elementId);
if (!el) return;
el.addEventListener('mouseenter', () => {
hoverTimer = setTimeout(() => {
vscode.postMessage({ type: 'proactiveTrigger', context: context });
}, 5000); // 5 seconds threshold
});
el.addEventListener('mouseleave', () => {
if (hoverTimer) clearTimeout(hoverTimer);
});
};
trackBehavior('settingsBtn', 'settings_exploration');
trackBehavior('brainBtn', 'brain_sync_exploration');
trackBehavior('agentSel', 'agent_selection_exploration');
</script>
</body>
</html>`;