From 332c35413e80a893443efe1f3097f0978d5dc3bf Mon Sep 17 00:00:00 2001 From: HighGarden Date: Fri, 23 Jan 2026 15:36:46 +0900 Subject: [PATCH 1/4] feat: add project workspace file preview and polish UI --- electron/main/config/provider-registry.ts | 12 +- electron/main/ipc/local-agents-handlers.ts | 22 +- electron/main/ipc/task-execution-handlers.ts | 39 +- electron/main/services/local-agent-session.ts | 168 +- package.json | 1 + pnpm-lock.yaml | 15 +- src/components/board/cards/AiTaskCard.vue | 80 +- src/components/board/cards/InputTaskCard.vue | 23 + src/components/board/cards/ScriptTaskCard.vue | 41 +- src/components/common/CodeEditor.vue | 4 +- src/components/common/UnifiedAISelector.vue | 1 + src/components/common/UpdateModal.vue | 16 +- src/components/project/OperatorPanel.vue | 1 + src/components/project/ProjectHeader.vue | 20 + src/components/project/ProjectInfoModal.vue | 511 ----- src/components/project/ProjectInfoPanel.vue | 1979 ++++++++++------- .../project/ProjectWorkspaceModal.vue | 388 ++++ .../project/ScriptTemplateModal.vue | 2 +- src/components/search/GlobalSearch.vue | 6 +- src/components/settings/AIProviderModal.vue | 10 +- src/components/settings/LocalAgentsTab.vue | 2 +- src/components/settings/MCPServerModal.vue | 10 +- src/components/settings/OperatorModal.vue | 2 +- src/components/setup/InitialSetupWizard.vue | 2 +- src/components/task/EnhancedResultPreview.vue | 276 ++- src/components/task/ResultPreviewPanel.vue | 2 +- src/components/task/SubdivisionModal.vue | 2 +- src/components/task/TaskCreateModal.vue | 8 +- src/components/task/TaskDetailPanel.vue | 686 ++++-- src/components/task/TaskEditModal.vue | 21 +- src/components/task/viewer/AgentViewer.vue | 193 ++ src/composables/task/useTaskStatus.ts | 2 + .../useConfigurationInheritance.ts | 2 +- src/composables/useLocalAgentExecution.ts | 17 +- src/core/types/ai.ts | 1 + src/core/types/database.ts | 39 +- .../marketplace/MarketplacePublishModal.vue | 2 +- .../marketplace/RegistrationWizard.vue | 2 +- src/renderer/stores/projectStore.ts | 16 +- src/renderer/stores/taskStore.ts | 19 + src/renderer/views/DAGView.vue | 23 +- src/renderer/views/KanbanBoardView.vue | 147 +- src/renderer/views/ProjectDetailView.vue | 31 +- src/renderer/views/ProjectsView.vue | 24 +- src/renderer/views/TimelineView.vue | 15 +- src/services/ai/AIConfig.ts | 8 +- src/services/ai/AIInterviewService.ts | 2 + src/services/ai/CuratorService.ts | 116 +- .../ai/parsers/GeminiManifestParser.ts | 157 ++ src/services/ai/providers/BaseAIProvider.ts | 19 +- src/services/ai/providers/ClaudeProvider.ts | 8 +- .../ai/providers/DefaultHighFlowProvider.ts | 20 +- src/services/ai/providers/GPTProvider.ts | 24 +- src/services/ai/providers/GeminiProvider.ts | 6 +- src/services/ai/providers/GroqProvider.ts | 6 +- src/services/ai/providers/LmStudioProvider.ts | 4 +- src/services/ai/providers/MistralProvider.ts | 12 +- src/services/ai/providers/ProviderFactory.ts | 18 + src/services/ai/utils/aiResultUtils.ts | 22 +- src/services/mcp/MCPManager.ts | 14 +- src/services/workflow/AIServiceManager.ts | 41 +- 61 files changed, 3396 insertions(+), 1964 deletions(-) delete mode 100644 src/components/project/ProjectInfoModal.vue create mode 100644 src/components/project/ProjectWorkspaceModal.vue create mode 100644 src/components/task/viewer/AgentViewer.vue create mode 100644 src/services/ai/parsers/GeminiManifestParser.ts diff --git a/electron/main/config/provider-registry.ts b/electron/main/config/provider-registry.ts index 915f0f5..ab47126 100644 --- a/electron/main/config/provider-registry.ts +++ b/electron/main/config/provider-registry.ts @@ -76,6 +76,13 @@ export const PROVIDER_REGISTRY: Record = { executionStrategy: 'local-session', agentCommand: 'codex', }, + 'gemini-cli': { + id: 'gemini-cli', + name: 'Gemini CLI', + type: 'local-agent', + executionStrategy: 'local-session', + agentCommand: 'gemini', + }, }; /** @@ -104,14 +111,15 @@ export function isApiProvider(providerId: string): boolean { /** * Get local agent type for provider */ -export function getLocalAgentType(providerId: string): 'claude' | 'codex' | null { +export function getLocalAgentType(providerId: string): 'claude' | 'codex' | 'gemini-cli' | null { const config = getProviderConfig(providerId); if (config?.type !== 'local-agent') return null; // Map provider ID to agent type - const agentMap: Record = { + const agentMap: Record = { 'claude-code': 'claude', codex: 'codex', + 'gemini-cli': 'gemini-cli', }; return agentMap[providerId] || null; diff --git a/electron/main/ipc/local-agents-handlers.ts b/electron/main/ipc/local-agents-handlers.ts index 3d955ae..b759907 100644 --- a/electron/main/ipc/local-agents-handlers.ts +++ b/electron/main/ipc/local-agents-handlers.ts @@ -43,7 +43,7 @@ async function checkCommandInstalled(command: string): Promise } // Whitelist of allowed commands for security - const allowedCommands = ['claude', 'codex', 'gemini']; + const allowedCommands = ['claude', 'codex', 'gemini', 'gemini-cli']; if (!allowedCommands.includes(command)) { console.warn(`Command not in whitelist: ${command}`); @@ -52,14 +52,24 @@ async function checkCommandInstalled(command: string): Promise return result; } + // Map internal IDs to actual CLI commands + const commandMap: Record = { + 'gemini-cli': 'gemini', + }; + + const actualCommand = commandMap[command] || command; + const enhancedPath = getEnhancedPath(); - // console.log(`[LocalAgents] Checking ${command} with PATH: ...`); + // console.log(`[LocalAgents] Checking ${actualCommand} with PATH: ...`); + + // Use actualCommand for execution + const cmdToCheck = actualCommand; let result: AgentCheckResult; try { // Try to get version using --version flag - const { stdout } = await execAsync(`${command} --version`, { + const { stdout } = await execAsync(`${cmdToCheck} --version`, { timeout: 5000, env: { ...process.env, PATH: enhancedPath }, }); @@ -76,14 +86,14 @@ async function checkCommandInstalled(command: string): Promise // If --version fails, try which/where command try { const whichCmd = process.platform === 'win32' ? 'where' : 'which'; - const { stdout } = await execAsync(`${whichCmd} ${command}`, { + const { stdout } = await execAsync(`${whichCmd} ${cmdToCheck}`, { timeout: 5000, env: { ...process.env, PATH: enhancedPath }, }); - console.log(`[LocalAgents] ${whichCmd} ${command} found:`, stdout.trim()); + console.log(`[LocalAgents] ${whichCmd} ${cmdToCheck} found:`, stdout.trim()); result = { installed: true }; } catch (whichError) { - console.log(`[LocalAgents] ${command} not found:`, (whichError as Error).message); + console.log(`[LocalAgents] ${cmdToCheck} not found:`, (whichError as Error).message); result = { installed: false }; } } diff --git a/electron/main/ipc/task-execution-handlers.ts b/electron/main/ipc/task-execution-handlers.ts index 964b12f..359625a 100644 --- a/electron/main/ipc/task-execution-handlers.ts +++ b/electron/main/ipc/task-execution-handlers.ts @@ -659,6 +659,17 @@ const triggerCurator = async ( // Fetch API keys from renderer localStorage to inject into Curator const win = getMainWindow(); let apiKeys: any = {}; + let defaultAiConfig: { providerId: string; modelId: string } | null = null; + let isLoggedIn = false; + + // Check login status (Main Process Source of Truth) + try { + const { getCurrentUser } = await import('../auth/google-oauth'); + const user = await getCurrentUser(); + isLoggedIn = !!user; + } catch (e) { + console.warn('[CuratorTrigger] Failed to check login status:', e); + } if (win) { try { @@ -687,6 +698,28 @@ const triggerCurator = async ( '[CuratorTrigger] Injected API keys (merged):', Object.keys(apiKeys).filter((k) => !!apiKeys[k]) ); + + // Determine Default AI Provider (First Enabled) + // Replicates settingsStore.ts logic + const enabledProvider = providers.find((p: any) => { + if (!p.enabled) return false; + if (['ollama', 'lmstudio', 'default-highflow'].includes(p.id)) { + return true; + } + // For others, require key or connection + return !!p.apiKey || p.isConnected; + }); + + if (enabledProvider) { + defaultAiConfig = { + providerId: enabledProvider.id, + modelId: enabledProvider.defaultModel, + }; + console.log( + '[CuratorTrigger] Detected User Default AI:', + defaultAiConfig + ); + } } } catch (e) { console.warn('[CuratorTrigger] Failed to fetch API keys from renderer:', e); @@ -696,8 +729,6 @@ const triggerCurator = async ( const curator = CuratorService.getInstance(); curator.setApiKeys(apiKeys); - curator.setApiKeys(apiKeys); - await curator.runCurator( projectId, sequence, @@ -706,7 +737,9 @@ const triggerCurator = async ( project, null, projectRepository, - preComputedContext + preComputedContext, + defaultAiConfig, + isLoggedIn ); // Update metadata with new hash to prevent duplicates diff --git a/electron/main/services/local-agent-session.ts b/electron/main/services/local-agent-session.ts index 8c1ff56..b410cc2 100644 --- a/electron/main/services/local-agent-session.ts +++ b/electron/main/services/local-agent-session.ts @@ -137,6 +137,7 @@ export class LocalAgentSession extends EventEmitter { private executionMode: 'persistent' | 'oneshot' = 'persistent'; private remoteThreadId: string | null = null; // Captured thread ID from agent private fullOutput: string = ''; // Capture full stdout for fallback + private lastErrorReason: string | null = null; // Capture specific error reason from agent events constructor( agentType: 'claude' | 'codex' | 'gemini-cli', @@ -165,6 +166,8 @@ export class LocalAgentSession extends EventEmitter { throw new Error('Session already started'); } + this.lastErrorReason = null; // Reset error reason + if (this.executionMode === 'oneshot') { console.log( `[LocalAgentSession] Starting ${this.agentType} session in one-shot mode (lazy spawn)` @@ -207,8 +210,21 @@ export class LocalAgentSession extends EventEmitter { throw new Error('Session not active'); } + this.lastErrorReason = null; // Reset error reason + if (this.status === 'running') { - throw new Error('Another message is being processed'); + // Safety check: if oneshot mode and process is missing/dead, force reset + if ( + this.executionMode === 'oneshot' && + (!this.process || this.process.killed || this.process.exitCode !== null) + ) { + console.warn( + `[LocalAgentSession] Session ${this.id} status was 'running' but process is dead/null. Resetting to idle.` + ); + this.status = 'idle'; + } else { + throw new Error('Another message is being processed'); + } } const timeout = options.timeout ?? 0; // 0 = unlimited @@ -253,10 +269,15 @@ export class LocalAgentSession extends EventEmitter { args.push('--dangerously-bypass-approvals-and-sandbox'); } else if (this.agentType === 'gemini-cli') { // Gemini CLI args - args.push('--json'); // Request JSON output + args.push('--output-format', 'stream-json'); // Request Streaming JSON output + args.push('--approval-mode', 'yolo'); // Auto-approve all tools if (options?.model) { args.push('--model', options.model); } + // Resume previous session if available + if (this.remoteThreadId) { + args.push('--resume', this.remoteThreadId); + } } else { // Default fallback (should vary by agent) args.push('exec'); @@ -408,8 +429,44 @@ export class LocalAgentSession extends EventEmitter { // Handle stderr this.process.stderr?.on('data', (data: Buffer) => { const text = data.toString(); + + // [Filter] Suppress known benign Node.js warnings from child process + if (text.includes('MaxListenersExceededWarning') && text.includes('AbortSignal')) { + return; + } + + // [Filter] Treat info messages printed to stderr as regular logs + if ( + text.includes('YOLO mode is enabled') || + text.includes('Loaded cached credentials') || + text.includes("Server 'sequential-thinking' supports tool updates") + ) { + console.log(`[LocalAgentSession] ${this.agentType} info:`, text); + return; + } + console.error(`[LocalAgentSession] ${this.agentType} stderr:`, text); - // Do NOT emit 'error' here as it crashes the app for non-fatal warnings + + // Fast-fail on 429 Rate Limit / Capacity errors for Gemini CLI + if (this.agentType === 'gemini-cli' || this.agentType === 'codex') { + if ( + text.includes('status 429') || + text.includes('RESOURCE_EXHAUSTED') || + text.includes('No capacity available') || + text.includes('429 Too Many Requests') + ) { + console.error( + `[LocalAgentSession] Detected 429 Rate Limit/Capacity Error for ${this.agentType}. Fast-failing.` + ); + this.lastErrorReason = + 'Quota exceeded or no capacity available (429). Please try again later or switch models.'; + + // Force kill the process immediately to stop internal tool retries + if (this.process && !this.process.killed) { + this.process.kill('SIGKILL'); + } + } + } }); // Handle process exit @@ -436,7 +493,12 @@ export class LocalAgentSession extends EventEmitter { // Only reject if not intentionally closing // Exit code 143 = 128 + 15 = SIGTERM (normal termination) if (this.currentReject && !this.isClosing) { - this.currentReject(new Error(`Process exited unexpectedly with code ${code}`)); + // Use captured error reason if available, otherwise generic message + const failureMessage = this.lastErrorReason + ? `Agent Failed: ${this.lastErrorReason}` + : `Process exited unexpectedly with code ${code}`; + + this.currentReject(new Error(failureMessage)); this.currentReject = null; this.currentResolve = null; this.currentOnChunk = null; @@ -542,39 +604,107 @@ export class LocalAgentSession extends EventEmitter { this.completeResponse({ ...message, finished: true }); } else if (message.type === 'error') { console.error('[LocalAgentSession] Codex Error Event:', message); + // Capture the error message + this.lastErrorReason = + typeof message.message === 'string' ? message.message : JSON.stringify(message); } return; } // Gemini CLI event handling if (this.agentType === 'gemini-cli') { - // Handle gemini-cli specific events - if ( - message.type === 'response' || - message.type === 'completion' || - message.text || - message.content - ) { - const textContent = String(message.text || message.content || ''); - if (textContent) { + const msg = message as any; + + // 1. Init event + if (msg.type === 'init') { + console.log(`[LocalAgentSession] Gemini CLI session started: ${msg.session_id}`); + if (msg.session_id) { + this.remoteThreadId = msg.session_id; + } + this.emit('started', this.getInfo()); + return; + } + + // 2. Message event (User or Assistant) + if (msg.type === 'message') { + const content = msg.content || ''; + + // Only emit assistant messages as 'message' events for the UI + if (msg.role === 'assistant') { const compatibleMsg = { type: 'assistant', message: { - content: [{ type: 'text', text: textContent }], + content: [{ type: 'text', text: content }], }, }; - this.captureTranscript(compatibleMsg); + + // Capture in transcript + this.captureTranscript({ + type: 'assistant', + message: { content: [{ type: 'text', text: content }] }, + }); + this.emit('message', compatibleMsg); - if (this.currentOnChunk) { - this.currentOnChunk(textContent); + if (this.currentOnChunk && msg.delta) { + // If delta is true, it might be a chunk, but the content seems to be full text in some contexts? + // Docs say "delta": true for simple chunks? + // The example "content": "Here are the files..." with delta: true implies a chunk. + this.currentOnChunk(content); + } else if (this.currentOnChunk && content) { + this.currentOnChunk(content); } } + return; } - if (message.done || message.finished || message.type === 'completion') { - this.completeResponse({ ...message, finished: true }); + // 3. Tool Use event + if (msg.type === 'tool_use') { + console.log(`[LocalAgentSession] Gemini Tool Use: ${msg.tool_name}`); + // Capture in transcript for history + this.transcript.push({ + role: 'assistant', + type: 'tool_use', + timestamp: new Date(msg.timestamp || Date.now()), + metadata: { + tool_name: msg.tool_name, + tool_id: msg.tool_id, + parameters: msg.parameters, + }, + }); + return; + } + + // 4. Tool Result event + if (msg.type === 'tool_result') { + console.log(`[LocalAgentSession] Gemini Tool Result: ${msg.status}`); + this.transcript.push({ + role: 'user', // Tool results are inputs to the model + type: 'tool_result', + timestamp: new Date(msg.timestamp || Date.now()), + metadata: { + tool_id: msg.tool_id, + status: msg.status, + output: msg.output, + }, + }); + return; } + + // 5. Error event + if (msg.type === 'error') { + console.error(`[LocalAgentSession] Gemini Error:`, msg); + // Non-fatal errors, just log them or emit as transient error? + // Depending on severity, we might want to fail the turn. + return; + } + + // 6. Result event (Completion) + if (msg.type === 'result') { + this.completeResponse({ ...msg, finished: true }); + return; + } + return; } diff --git a/package.json b/package.json index 115f691..883386a 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.26.0", "@google/genai": "^1.31.0", + "@headlessui/vue": "^1.7.23", "@iconify/vue": "^5.0.0", "@mistralai/mistralai": "^1.11.0", "@modelcontextprotocol/sdk": "^0.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f71152..259fa90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ dependencies: '@google/genai': specifier: ^1.31.0 version: 1.34.0(@modelcontextprotocol/sdk@0.5.0) + '@headlessui/vue': + specifier: ^1.7.23 + version: 1.7.23(vue@3.5.26) '@iconify/vue': specifier: ^5.0.0 version: 5.0.0(vue@3.5.26) @@ -1821,6 +1824,16 @@ packages: - utf-8-validate dev: false + /@headlessui/vue@1.7.23(vue@3.5.26): + resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==} + engines: {node: '>=10'} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@tanstack/vue-virtual': 3.13.13(vue@3.5.26) + vue: 3.5.26(typescript@5.9.3) + dev: false + /@hono/node-server@1.19.7(hono@4.11.1): resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} engines: {node: '>=18.14.1'} @@ -2987,7 +3000,6 @@ packages: /@tanstack/virtual-core@3.13.13: resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==} - dev: true /@tanstack/vue-virtual@3.13.13(vue@3.5.26): resolution: {integrity: sha512-Cf2xIEE8nWAfsX0N5nihkPYMeQRT+pHt4NEkuP8rNCn6lVnLDiV8rC8IeIxbKmQC0yPnj4SIBLwXYVf86xxKTQ==} @@ -2996,7 +3008,6 @@ packages: dependencies: '@tanstack/virtual-core': 3.13.13 vue: 3.5.26(typescript@5.9.3) - dev: true /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} diff --git a/src/components/board/cards/AiTaskCard.vue b/src/components/board/cards/AiTaskCard.vue index d8b628c..fae6fd7 100644 --- a/src/components/board/cards/AiTaskCard.vue +++ b/src/components/board/cards/AiTaskCard.vue @@ -187,7 +187,7 @@ const hasPreviousResult = computed(() => { const isLocalProvider = computed(() => { const provider = assignedOperator.value?.aiProvider || props.task.aiProvider; - return ['claude-code', 'codex'].includes(provider || ''); + return ['claude-code', 'gemini-cli', 'codex'].includes(provider || ''); }); const displayModel = computed(() => { @@ -304,6 +304,24 @@ function handleConnectProviderClick() { emit('connectProvider', props.missingProvider.id); } } +// Error Message Computation +const errorMessage = computed(() => { + const t = props.task as any; + if (props.task.status === 'blocked') { + return props.task.blockedReason || t.error || 'Task execution blocked'; + } + if (props.task.status === 'failed') { + return ( + t.executionResult?.error || + t.error || + t.result || + t.errorMessage || + 'Task execution failed' + ); + } + return null; +}); + function hexToRgba(hex: string, alpha: number) { // Remove hash if present hex = hex.replace('#', ''); @@ -929,7 +947,6 @@ function hexToRgba(hex: string, alpha: number) { {{ task.reviewFailed ? '실패 분석' : t('project.actions.view_result') }} + + + diff --git a/src/components/board/cards/ScriptTaskCard.vue b/src/components/board/cards/ScriptTaskCard.vue index 5079fd6..2e12e7e 100644 --- a/src/components/board/cards/ScriptTaskCard.vue +++ b/src/components/board/cards/ScriptTaskCard.vue @@ -484,10 +484,9 @@ function handleViewScript(event: Event) { - - diff --git a/src/components/common/CodeEditor.vue b/src/components/common/CodeEditor.vue index 168b047..abff3b4 100644 --- a/src/components/common/CodeEditor.vue +++ b/src/components/common/CodeEditor.vue @@ -310,8 +310,8 @@ defineExpose({ diff --git a/src/components/common/UnifiedAISelector.vue b/src/components/common/UnifiedAISelector.vue index 78faae4..4c5e517 100644 --- a/src/components/common/UnifiedAISelector.vue +++ b/src/components/common/UnifiedAISelector.vue @@ -56,6 +56,7 @@ const availableLocalAgents = computed(() => { version?: string; }[] = [ { id: 'claude', name: 'Claude Code', icon: '🤖', installed: false }, + { id: 'gemini-cli', name: 'Gemini CLI', icon: '✨', installed: false }, { id: 'codex', name: 'OpenAI Codex', icon: '💻', installed: false }, ]; diff --git a/src/components/common/UpdateModal.vue b/src/components/common/UpdateModal.vue index 1eeff98..951dcfb 100644 --- a/src/components/common/UpdateModal.vue +++ b/src/components/common/UpdateModal.vue @@ -48,18 +48,20 @@ function preventClose(event: Event) { >
-
-
-
+
+
+
-
+
+ +

{{ viewLabel }}

diff --git a/src/components/project/ProjectInfoModal.vue b/src/components/project/ProjectInfoModal.vue deleted file mode 100644 index e490ba5..0000000 --- a/src/components/project/ProjectInfoModal.vue +++ /dev/null @@ -1,511 +0,0 @@ - - - - - diff --git a/src/components/project/ProjectInfoPanel.vue b/src/components/project/ProjectInfoPanel.vue index de615ea..3a08dc6 100644 --- a/src/components/project/ProjectInfoPanel.vue +++ b/src/components/project/ProjectInfoPanel.vue @@ -10,8 +10,9 @@ * - Cost and token usage statistics */ -import { computed, ref, watch } from 'vue'; +import { computed, ref, watch, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import ProjectMemoryPanel from './ProjectMemoryPanel.vue'; import { marked } from 'marked'; import type { MCPConfig, Project } from '@core/types/database'; @@ -37,6 +38,7 @@ function isLocalAgentProvider(provider: string | null): { const localAgentMap: Record = { 'claude-code': 'claude', codex: 'codex', + 'gemini-cli': 'gemini-cli', }; const agentType = localAgentMap[provider]; @@ -58,26 +60,37 @@ function isLocalAgentProvider(provider: string | null): { const props = defineProps<{ project: Project; + show?: boolean; // Controls visibility compact?: boolean; }>(); const emit = defineEmits<{ + (e: 'close'): void; (e: 'edit'): void; (e: 'open-output'): void; - (e: 'update-guidelines', guidelines: string): void; - (e: 'update-base-folder', folder: string): void; - ( - e: 'update-ai-settings', - settings: { aiProvider: string | null; aiModel: string | null } - ): void; - (e: 'update-output-type', type: string | null): void; - ( - e: 'update-auto-review-settings', - settings: { aiProvider: string | null; aiModel: string | null } - ): void; - (e: 'update-mcp-config', config: MCPConfig | null): void; }>(); +// Panel state +const activeTab = ref<'info' | 'context'>('info'); + +function handleClose() { + emit('close'); +} + +// Watch for show prop to handle body scroll or ESC key if needed +watch( + () => props.show, + (show, _, onCleanup) => { + if (show) { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') handleClose(); + }; + window.addEventListener('keydown', handleEscape); + onCleanup(() => window.removeEventListener('keydown', handleEscape)); + } + } +); + // ======================================== // State // ======================================== @@ -142,11 +155,16 @@ const aiProviderDisplay = computed(() => { const providerId = isEditingAI.value ? editedAIProvider.value : effectiveAI.value.provider; // Check if it's a local agent - if (providerId && ['claude-code', 'codex'].includes(providerId)) { + if (providerId && isLocalAgentProvider(providerId).isLocal) { + const localIcons: Record = { + 'claude-code': '🤖', + codex: '💻', + 'gemini-cli': '✨', + }; return { name: getAssistantLabel(providerId), color: 'text-gray-400', - icon: getAssistantIcon(providerId), + icon: localIcons[providerId] || '💻', }; } @@ -358,7 +376,7 @@ const autoReviewProviderDisplay = computed(() => { effectiveAutoReview.value.provider; // Check if it's a local agent - if (providerId && ['claude-code', 'codex'].includes(providerId)) { + if (providerId && isLocalAgentProvider(providerId).isLocal) { return { name: getAssistantLabel(providerId), color: 'text-gray-400', @@ -411,11 +429,18 @@ watch( // Methods // ======================================== +const titleInputRef = ref(null); + function startEditMetadata(): void { editedTitle.value = props.project.title; // Assuming project has emoji field, cast to any if TS complains or update Project type if possible editedEmoji.value = (props.project as any).emoji || ''; isEditingMetadata.value = true; + + // Focus title input + nextTick(() => { + titleInputRef.value?.focus(); + }); } function cancelEditMetadata(): void { @@ -463,9 +488,18 @@ function cancelEditGuidelines(): void { editedGuidelines.value = ''; } -function saveGuidelines(): void { - emit('update-guidelines', editedGuidelines.value); - isEditingGuidelines.value = false; +async function saveGuidelines(): Promise { + try { + const projectStore = useProjectStore(); + await projectStore.updateProject(props.project.id, { + aiGuidelines: editedGuidelines.value || null, + }); + isEditingGuidelines.value = false; + // Optimization: update local effective value display immediately if needed, + // but store reactivity should handle it via props.project watcher/computed + } catch (error) { + console.error('Failed to update guidelines:', error); + } } function copyGuidelines(): void { @@ -474,15 +508,22 @@ function copyGuidelines(): void { } } -function saveBaseFolder(): void { - emit('update-base-folder', editedBaseFolder.value); +async function saveBaseFolder(): Promise { + try { + const projectStore = useProjectStore(); + await projectStore.updateProject(props.project.id, { + baseDevFolder: editedBaseFolder.value || null, + }); + } catch (error) { + console.error('Failed to update base folder:', error); + } } async function pickBaseFolder(): Promise { const dir = await (window as any)?.electron?.fs?.selectDirectory?.(); if (dir) { editedBaseFolder.value = dir; - saveBaseFolder(); + await saveBaseFolder(); } } @@ -528,31 +569,31 @@ async function saveAISettings(): Promise { const agentMap: Record = { claude: 'claude-code', codex: 'codex', + 'gemini-cli': 'gemini-cli', }; providerToSave = (agentMap[editedLocalAgent.value] || editedLocalAgent.value) as AIProvider; // For local agents, the model might be redundant or same as provider ID, but let's keep it clean modelToSave = null; } - // Emit settings update - emit('update-ai-settings', { - aiProvider: providerToSave, - aiModel: modelToSave, - }); + try { + const projectStore = useProjectStore(); + await projectStore.updateProject(props.project.id, { + aiProvider: providerToSave, + aiModel: modelToSave, + }); - // If project was synced with Claude, mark as manually overridden - if (wasClaudeCodeSynced) { - try { - const projectStore = useProjectStore(); + // If project was synced with Claude, mark as manually overridden + if (wasClaudeCodeSynced) { const overrideUpdate = projectClaudeSyncService.markAsOverridden(props.project as any); await projectStore.updateProject(props.project.id, overrideUpdate as any); console.log('[ProjectInfoPanel] Marked project settings as manually overridden'); - } catch (error) { - console.error('[ProjectInfoPanel] Failed to mark as overridden:', error); } - } - isEditingAI.value = false; + isEditingAI.value = false; + } catch (error) { + console.error('Failed to update AI settings:', error); + } } // MCP 관련 함수 @@ -569,9 +610,33 @@ function cancelEditMCP(): void { editedMCPConfig.value = null; } -function saveMCPSettings(): void { - emit('update-mcp-config', editedMCPConfig.value); - isEditingMCP.value = false; +async function saveMCPSettings(): Promise { + try { + const projectStore = useProjectStore(); + // Deep clone to ensure no proxies are passed + const configToSave = editedMCPConfig.value + ? JSON.parse(JSON.stringify(editedMCPConfig.value)) + : {}; + + // Auto-configure filesystem MCP if enabled and baseDevFolder is set + if (configToSave['filesystem'] && props.project.baseDevFolder) { + configToSave['filesystem'].config = { + ...(configToSave['filesystem'].config || {}), + args: [ + '-y', + '@modelcontextprotocol/server-filesystem', + props.project.baseDevFolder, + ], + }; + } + + await projectStore.updateProject(props.project.id, { + mcpConfig: configToSave, + }); + isEditingMCP.value = false; + } catch (error) { + console.error('Failed to update MCP settings:', error); + } } // Output Type Methods @@ -585,9 +650,16 @@ function cancelEditOutputType(): void { editedOutputType.value = null; } -function saveOutputType(): void { - emit('update-output-type', editedOutputType.value); - isEditingOutputType.value = false; +async function saveOutputType(): Promise { + try { + const projectStore = useProjectStore(); + await projectStore.updateProject(props.project.id, { + outputType: editedOutputType.value || null, + }); + isEditingOutputType.value = false; + } catch (error) { + console.error('Failed to update output type:', error); + } } // Goal Methods @@ -644,12 +716,21 @@ function cancelEditAutoReview(): void { editedAutoReviewLocalAgent.value = null; } -function saveAutoReviewSettings(): void { - emit('update-auto-review-settings', { - aiProvider: editedAutoReviewProvider.value, - aiModel: editedAutoReviewModel.value, - }); - isEditingAutoReview.value = false; +async function saveAutoReviewSettings(): Promise { + try { + const projectStore = useProjectStore(); + const currentMetadata = (props.project as any).metadata || {}; + await projectStore.updateProject(props.project.id, { + metadata: { + ...currentMetadata, + autoReviewProvider: editedAutoReviewProvider.value, + autoReviewModel: editedAutoReviewModel.value, + }, + }); + isEditingAutoReview.value = false; + } catch (error) { + console.error('Failed to update auto-review settings:', error); + } } // Helper functions for displaying assistant types @@ -660,6 +741,8 @@ function getAssistantIcon(type: string): string { 'claude-code': 'M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm88,104a87.62,87.62,0,0,1-6.4,32.94l-44.7-27.49a15.92,15.92,0,0,0-6.24-2.23l-22.82-3.08a16.11,16.11,0,0,0-16,7.86h-8.72l-3.8-7.86a15.91,15.91,0,0,0-11.89-8.42l-22.26-3a16.09,16.09,0,0,0-13.38,4.93L40,132.19A88,88,0,0,1,128,40a87.53,87.53,0,0,1,15.87,1.46L159.3,56a16,16,0,0,0,12.26,5.61h19.41A88.22,88.22,0,0,1,216,128Z', codex: 'M229.66,90.34l-64-64a8,8,0,0,0-11.32,0l-64,64a8,8,0,0,0,11.32,11.32L152,51.31V96a8,8,0,0,0,16,0V51.31l50.34,50.35a8,8,0,0,0,11.32-11.32ZM208,144a40,40,0,1,0-40,40A40,40,0,0,0,208,144Zm-64,0a24,24,0,1,1,24,24A24,24,0,0,1,144,144ZM88,104A40,40,0,1,0,48,144,40,40,0,0,0,88,104ZM64,144a24,24,0,1,1,24-24A24,24,0,0,1,64,144Zm176,72a40,40,0,1,0-40,40A40,40,0,0,0,240,216Zm-64,0a24,24,0,1,1,24,24A24,24,0,0,1,176,216Z', + 'gemini-cli': + 'M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z', }; return (icons[type] || icons.git) as string; } @@ -668,6 +751,7 @@ function getAssistantLabel(type: string): string { const labels: Record = { git: 'Git', 'claude-code': 'Claude Code', + 'gemini-cli': 'Gemini CLI', codex: 'Codex', cursor: 'Cursor', @@ -680,860 +764,1049 @@ function getAssistantLabel(type: string): string { + + + diff --git a/src/components/task/ResultPreviewPanel.vue b/src/components/task/ResultPreviewPanel.vue index f7a9f17..ace081b 100644 --- a/src/components/task/ResultPreviewPanel.vue +++ b/src/components/task/ResultPreviewPanel.vue @@ -168,7 +168,7 @@ function formatDate(date: Date): string {
diff --git a/src/components/task/SubdivisionModal.vue b/src/components/task/SubdivisionModal.vue index 4b2117c..6d8bbf6 100644 --- a/src/components/task/SubdivisionModal.vue +++ b/src/components/task/SubdivisionModal.vue @@ -106,7 +106,7 @@ function handleClose() {