diff --git a/jest.config.js b/jest.config.js index 145d7ac..eead2a4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,9 @@ module.exports = { testEnvironment: 'node', testMatch: ['**/tests/**/*.test.ts'], moduleFileExtensions: ['ts', 'js'], + moduleNameMapper: { + '^vscode$': '/tests/mocks/vscode.js', + }, transform: { '^.+\\.ts$': 'ts-jest', }, diff --git a/package.json b/package.json index 62a49ae..f1e6833 100644 --- a/package.json +++ b/package.json @@ -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.0", + "version": "2.33.1", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/core/dataProcessor.ts b/src/core/dataProcessor.ts index 94d8b48..9717473 100644 --- a/src/core/dataProcessor.ts +++ b/src/core/dataProcessor.ts @@ -15,6 +15,10 @@ export interface AggregateResult { average?: number; } +export interface AggregateOptions { + collectValues?: boolean; +} + /** * DataProcessor: * 시스템의 알고리즘적 효율성과 유지보수성을 극대화하기 위한 핵심 집계 엔진. @@ -26,8 +30,14 @@ export class DataProcessor { * @param data 집계할 데이터 배열 * @param keyPath 집계 기준이 될 속성 경로 */ - public static aggregate(data: any[], keyPath: string): AggregateResult[] { + public static aggregate(data: any[], keyPath: string, options: AggregateOptions = {}): AggregateResult[] { if (!data || data.length === 0) return []; + if (!Array.isArray(data)) { + throw new TypeError('DataProcessor.aggregate expects data to be an array.'); + } + + const pathSegments = this.parseKeyPath(keyPath); + const collectValues = options.collectValues !== false; // 1. 성능 상충 관계 (Sweet Spot) 고려: // 데이터가 매우 작을 때는(예: N < 10) 해시 맵 생성 오버헤드가 더 클 수 있으나, @@ -37,7 +47,7 @@ export class DataProcessor { for (const item of data) { try { - const keyValue = this.getNestedValue(item, keyPath); + const keyValue = this.getNestedValue(item, pathSegments); if (keyValue === undefined || keyValue === null) continue; const key = String(keyValue); @@ -53,7 +63,9 @@ export class DataProcessor { } entry.count++; - entry.values.push(item); + if (collectValues) { + entry.values.push(item); + } // 수치형 데이터인 경우 평균 계산을 위한 로직 (예시) if (typeof item.value === 'number') { @@ -74,15 +86,37 @@ export class DataProcessor { * 데이터 분포 민감성(Data Distribution Sensitivity)을 고려한 고도화된 집계 (Trie 기반) * 키가 매우 길거나 계층적인 경우 메모리 및 검색 속도 최적화를 위해 사용합니다. */ - public static aggregateByTrie(data: any[], keyPath: string): AggregateResult[] { + public static aggregateByTrie(data: any[], keyPath: string, options: AggregateOptions = {}): AggregateResult[] { // TODO: 복잡한 키 구조를 위한 Trie 인덱싱 로직 구현 (Phase 2 확장 예정) - return this.aggregate(data, keyPath); + return this.aggregate(data, keyPath, options); } /** - * 중첩된 객체 속성 접근 (Safety handling) + * 중첩된 객체 속성 접근 (Safety handling). + * keyPath는 루프 밖에서 한 번만 파싱하여 대규모 데이터 처리 시 반복 split 비용을 피합니다. */ - private static getNestedValue(obj: any, path: string): any { - return path.split('.').reduce((prev, curr) => prev && prev[curr], obj); + private static getNestedValue(obj: any, pathSegments: string[]): any { + return pathSegments.reduce((prev, curr) => { + if (prev === undefined || prev === null) return undefined; + return prev[curr]; + }, obj); + } + + private static parseKeyPath(keyPath: string): string[] { + if (typeof keyPath !== 'string' || keyPath.trim().length === 0) { + throw new TypeError('DataProcessor.aggregate expects keyPath to be a non-empty string.'); + } + + const segments = keyPath.split('.').map(segment => segment.trim()); + if (segments.some(segment => segment.length === 0)) { + throw new TypeError('DataProcessor.aggregate received an invalid keyPath.'); + } + + const forbiddenSegments = new Set(['__proto__', 'prototype', 'constructor']); + if (segments.some(segment => forbiddenSegments.has(segment))) { + throw new TypeError('DataProcessor.aggregate received an unsafe keyPath.'); + } + + return segments; } } diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 11096c0..2f5cc77 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -97,6 +97,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn case 'getModels': await this._sendModels(); break; + case 'getSessions': + await this._sendSessionList(); + break; case 'getAgents': await this._sendAgentsList(); break; @@ -868,6 +871,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn --table-header-bg: #161b22; --table-row-hover: #21262d; --input-bg: #0d1117; + --control-bg: #161b22; + --control-bg-hover: #21262d; + --control-active-bg: rgba(88, 166, 255, 0.14); + --shadow-soft: 0 8px 24px rgba(0, 0, 0, 0.18); } body.vscode-light { @@ -888,6 +895,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn --table-header-bg: #f6f8fa; --table-row-hover: #f3f4f6; --input-bg: #ffffff; + --control-bg: #ffffff; + --control-bg-hover: #f6f8fa; + --control-active-bg: rgba(9, 105, 218, 0.1); + --shadow-soft: 0 8px 20px rgba(31, 35, 40, 0.08); } * { margin: 0; padding: 0; box-sizing: border-box; } @@ -918,20 +929,57 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn display: flex; align-items: center; justify-content: space-between; - padding: 10px 14px; + padding: 10px 12px 8px; + gap: 10px; } .header-controls { - display: flex; - align-items: center; - gap: 6px; - padding: 0 14px 10px; - flex-wrap: wrap; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 8px; + padding: 0 12px 12px; } - .brand { font-weight: 700; font-size: 14px; color: var(--text-bright); letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; } + .header-actions, + .tool-strip, + .select-stack, + .select-line, + .status-pill { + display: flex; + align-items: center; + } + + .header-actions { gap: 6px; flex-shrink: 0; } + .tool-strip { gap: 6px; flex-wrap: wrap; } + .select-stack { flex-direction: column; gap: 6px; min-width: 0; } + .select-line { gap: 6px; width: 100%; min-width: 0; } + + .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; } + .status-pill { + height: 28px; + gap: 6px; + padding: 0 9px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--control-bg); + color: var(--text-dim); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + flex-shrink: 0; + } + + .status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text-dim); + box-shadow: 0 0 0 2px rgba(139, 148, 158, 0.12); + flex-shrink: 0; + } + .chat { flex: 1; overflow-y: auto; @@ -955,8 +1003,6 @@ 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; } - .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; } @@ -969,8 +1015,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn } [data-tooltip]:hover::after { opacity: 1; bottom: -35px; } - .header-controls { display: flex; gap: 8px; margin-left: auto; } - #input::placeholder { color: var(--accent); opacity: 0.6; font-weight: 500; } @@ -1030,13 +1074,63 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn .action-btn:hover { color: var(--text-bright); border-color: var(--accent); background: var(--accent-glow); } .icon-btn { - background: var(--surface); border: 1px solid var(--border); color: var(--text-dim); width: 28px; height: 28px; - border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; font-size: 13px; + background: var(--control-bg); + border: 1px solid var(--border); + color: var(--text-dim); + width: 28px; + height: 28px; + border-radius: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; + font-size: 12px; + font-weight: 700; + line-height: 1; + flex: 0 0 28px; } - .icon-btn:hover { color: var(--text-bright); border-color: var(--accent); background: var(--accent-glow); } - .icon-btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-glow); } + .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); } - select { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); padding: 4px 8px; border-radius: 6px; font-size: 10.5px; cursor: pointer; } + .select-wrap { + position: relative; + min-width: 0; + flex: 1 1 0; + } + + .select-wrap::after { + content: ''; + position: absolute; + right: 10px; + top: 50%; + width: 6px; + height: 6px; + border-right: 1.5px solid var(--text-dim); + border-bottom: 1.5px solid var(--text-dim); + transform: translateY(-65%) rotate(45deg); + pointer-events: none; + } + + select { + width: 100%; + min-width: 0; + height: 30px; + background: var(--control-bg); + color: var(--text-primary); + border: 1px solid var(--border); + padding: 0 28px 0 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + outline: none; + appearance: none; + transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; + } + + select:hover { border-color: var(--border-bright); background: var(--control-bg-hover); } + select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } /* --- Input & Attachments --- */ .input-wrap { @@ -1178,6 +1272,56 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn .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); } + .panel { + display: none; + flex-direction: column; + gap: 8px; + padding-bottom: 10px; + } + + .field-label { + color: var(--text-dim); + font-size: 10px; + font-weight: 700; + line-height: 1.2; + } + + .panel textarea { + font-size: 11.5px; + padding: 8px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--input-bg); + color: var(--text-bright); + width: 100%; + resize: vertical; + font-family: inherit; + outline: none; + } + + .panel textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); + } + + .secondary-btn { + height: 30px; + background: var(--control-bg); + border: 1px solid var(--border); + color: var(--text-primary); + padding: 0 10px; + font-size: 11px; + font-weight: 700; + border-radius: 6px; + cursor: pointer; + } + + .secondary-btn:hover { + border-color: var(--accent); + background: var(--control-active-bg); + color: var(--text-bright); + } + /* --- Physics & Micro-interactions --- */ button { transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); @@ -1288,6 +1432,12 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn color: var(--text-dim); padding-left: 20px; } + @media (min-width: 360px) { + .header-controls { + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + } + } @keyframes slideIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } @@ -1298,22 +1448,30 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
G1nation
-
- - +
+ +
-
- - - - - - - - - +
+
+
Engine
+
+
+
+
+
+
+
+
+ + + + + + +
@@ -1345,14 +1503,14 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
-