diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index eadc9144..63d3be44 100644 --- a/CODEBASE_DOCUMENTATION.md +++ b/CODEBASE_DOCUMENTATION.md @@ -465,6 +465,9 @@ POST /api/discord/process-queue - Dispatch queue processing prompt with optio POST /api/sessions/intent-haiku - Generate <=200 char intent summary for an active Claude/Codex session GET /api/greenfield/categories - Greenfield category list (taxonomy-backed) POST /api/greenfield/detect-category - Infer category from description (taxonomy keyword matching) +GET /api/setup-actions - List Windows dependency-onboarding actions +GET /api/setup-actions/state - Read persisted dependency-onboarding state (completed/dismissed/current step) +PUT /api/setup-actions/state - Persist dependency-onboarding state into app data for desktop restarts GET /api/user-settings - Get user preferences PUT /api/user-settings - Update user preferences @@ -538,5 +541,17 @@ LOGGING: Winston-based structured logging with rotation 9. **Mixed-repo workspaces**: Terminal naming must avoid conflicts between repos 10. **Template validation**: Always validate workspace templates against schemas + +## First-Run Dependency Onboarding (Windows) + +``` +server/setupActionService.js - Defines setup actions and launches PowerShell installers +server/onboardingStateService.js - Persists Windows dependency-onboarding state in app data so Tauri restarts survive per-launch localhost ports +server/index.js - Routes: GET/PUT /api/setup-actions/state plus setup action execution endpoints +client/app.js - Guided dependency onboarding steps + diagnostics integration +client/index.html - Dependency onboarding modal markup + launch button +client/styles.css - Dependency onboarding progress/step styling +``` + --- ๐Ÿšจ **END OF FILE - ENSURE YOU READ EVERYTHING ABOVE** ๐Ÿšจ diff --git a/client/app.js b/client/app.js index c9936ba9..9a0cccdb 100644 --- a/client/app.js +++ b/client/app.js @@ -845,7 +845,7 @@ class ClaudeOrchestrator { if (!expiresAt || expiresAt <= now) this.pendingWorktreeReservations.delete(key); } } - + async init() { try { // Initialize managers @@ -984,21 +984,21 @@ class ClaudeOrchestrator { if (this.settings.notifications) { this.notificationManager.requestPermission(); } - + // Set up UI this.setupEventListeners(); this.applyTheme(); this.syncSettingsUI(); this.applySimpleModeConfig(); this.installAuthFetchShim(); - + // Connect to server await this.connectToServer(); await this.ensureProjectTypeTaxonomy(); // Hook panels that depend on socket events this.activityFeedPanel?.onSocketConnected?.(this.socket); - + // Load user settings from server await this.loadUserSettings(); this.applySidebarDesktopCollapsedFromPrefs(); @@ -1016,22 +1016,22 @@ class ClaudeOrchestrator { // WIP / Queue banner (process status) this.startProcessStatusBanner(); - + // Check for updates on startup this.checkForSettingsUpdates(); - + // Hide loading message if it exists const loadingMessage = document.getElementById('loading-message'); if (loadingMessage) { loadingMessage.classList.add('hidden'); } - + } catch (error) { console.error('Failed to initialize:', error); this.showError('Failed to initialize application'); } } - + async connectToServer() { return new Promise((resolve, reject) => { console.log('Attempting to connect to server...'); @@ -1044,29 +1044,29 @@ class ClaudeOrchestrator { const serverUrl = window.location.origin; this.socket = io(serverUrl, socketOptions); console.log(`Socket connecting to ${serverUrl}...`); - + // Connection events this.socket.on('connect', () => { console.log('Connected to server'); this.updateConnectionStatus(true); resolve(); }); - + this.socket.on('connect_error', (error) => { console.error('Connection error:', error); this.updateConnectionStatus(false); - + if (error.message === 'Authentication failed') { this.showError('Authentication failed. Please check your token.'); } reject(error); }); - + this.socket.on('disconnect', () => { console.log('Disconnected from server'); this.updateConnectionStatus(false); }); - + // Session events this.socket.on('sessions', async (sessionStates) => { console.log('Received sessions event:', sessionStates); @@ -1085,7 +1085,7 @@ class ClaudeOrchestrator { this.worktreeTags.set(worktreePath, tag || {}); this.buildSidebar(); }); - + this.socket.on('terminal-output', ({ sessionId, data }) => { this.terminalManager.handleOutput(sessionId, data); @@ -1133,7 +1133,7 @@ class ClaudeOrchestrator { this.sessionActivity.set(sessionId, 'active'); } }); - + this.socket.on('autosuggest-response', ({ sessionId, suggestion, prefix }) => { this.terminalManager.handleAutosuggestResponse(sessionId, suggestion, prefix); }); @@ -1142,15 +1142,15 @@ class ClaudeOrchestrator { this.updateSessionStatus(sessionId, status); this.maybeAutoSendPrompt(sessionId, status); }); - + this.socket.on('branch-update', ({ sessionId, branch, remoteUrl, defaultBranch, existingPR }) => { this.updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR); }); - + this.socket.on('notification-trigger', (notification) => { this.notificationManager.handleNotification(notification); }); - + this.socket.on('session-exited', ({ sessionId, exitCode }) => { this.handleSessionExit(sessionId, exitCode); }); @@ -1343,7 +1343,7 @@ class ClaudeOrchestrator { // Hide + persist dismissal so it doesn't resurrect on refresh/worktree-add this.hideStartupUI(sessionId); this.scheduleAutoPromptFallback(sessionId, 'claude'); - + // Enable the start button now that Claude has started const startBtn = document.getElementById(`claude-start-btn-${sessionId}`); if (startBtn) { @@ -1361,7 +1361,7 @@ class ClaudeOrchestrator { this.socket.on('claude-update-required', (updateInfo) => { this.showClaudeUpdateRequired(updateInfo); }); - + this.socket.on('user-settings-updated', (settings) => { console.log('User settings updated:', settings); this.userSettings = settings; @@ -1589,7 +1589,7 @@ class ClaudeOrchestrator { this.socket.on('git-updated', (result) => { console.log('Git updated:', result); this.showTemporaryMessage(`Repository updated successfully! ${result.wasUpToDate ? 'Already up to date.' : 'Changes pulled.'}`, 'success'); - + // Refresh the page after successful update if (!result.wasUpToDate) { setTimeout(() => { @@ -1600,45 +1600,45 @@ class ClaudeOrchestrator { }, 3000); } }); - + // Build production events this.socket.on('build-started', ({ sessionId, worktreeNum }) => { console.log(`Build started for worktree ${worktreeNum}`); }); - + this.socket.on('build-completed', ({ sessionId, worktreeNum, zipPath }) => { console.log(`Build completed for worktree ${worktreeNum}: ${zipPath}`); - + // Restore the build button (use work{num} pattern to find buttons) this.restoreBuildButton(`work${worktreeNum}`); - + // Request to reveal the file in explorer this.socket.emit('reveal-in-explorer', { path: zipPath }); }); - + this.socket.on('build-failed', ({ sessionId, worktreeNum, error }) => { console.error(`Build failed for worktree ${worktreeNum}:`, error); this.showError(`โŒ Build failed for Worktree ${worktreeNum}: ${error}`); - + // Restore the build button (use work{num} pattern to find buttons) this.restoreBuildButton(`work${worktreeNum}`); }); - + // Periodic heartbeat to keep sessions alive while UI is open this.startHeartbeats(); - + this.socket.on('server-started', ({ sessionId, port }) => { console.log(`[SERVER-STARTED EVENT] Session: ${sessionId}, Port: ${port}`); this.serverPorts.set(sessionId, port); console.log(`Server ${sessionId} started on port ${port}`); console.log('Current serverPorts:', Array.from(this.serverPorts.entries())); - + // Only open localhost automatically - Hytopia needs manual click due to popup blockers setTimeout(() => { const localhostUrl = `https://localhost:${port}`; console.log(`Opening localhost for initialization: ${localhostUrl}`); window.open(localhostUrl, '_blank'); - + // Show notification that server is ready if (this.settings.notifications) { this.showNotification('Server Ready', `Server ${sessionId.replace('-server', '')} is running on port ${port}. Click ๐ŸŽฎ to play!`); @@ -1657,14 +1657,14 @@ class ClaudeOrchestrator { reject(new Error('Connection timeout')); } }, 10000); - + // Clear timeout on successful connection this.socket.on('connect', () => { clearTimeout(timeoutId); }); }); } - + startHeartbeats() { if (this._heartbeatInterval) { clearInterval(this._heartbeatInterval); @@ -1676,7 +1676,7 @@ class ClaudeOrchestrator { } }, 30000); } - + setupEventListeners() { // Check if elements exist before adding listeners const elements = { @@ -1739,7 +1739,7 @@ class ClaudeOrchestrator { 'start-claude', 'cancel-claude-startup' ]); - + // Check all elements exist for (const id in elements) { elements[id] = document.getElementById(id); @@ -1747,7 +1747,7 @@ class ClaudeOrchestrator { console.warn(`Element not found: ${id}`); } } - + // Sidebar worktree clicks - use toggle instead of show if (elements['worktree-list']) { elements['worktree-list'].addEventListener('click', (e) => { @@ -1856,7 +1856,7 @@ class ClaudeOrchestrator { if (!document.body.classList.contains('sidebar-open')) return; this.closeSidebar(); }); - + // View buttons const dashboardBtn = document.getElementById('dashboard-btn'); if (dashboardBtn) { @@ -1878,26 +1878,26 @@ class ClaudeOrchestrator { this.setViewMode('all'); if (this.isMobileLayout()) this.closeSidebar(); }); - + document.getElementById('view-claude-only').addEventListener('click', () => { this.setViewMode('claude'); if (this.isMobileLayout()) this.closeSidebar(); }); - + document.getElementById('view-servers-only').addEventListener('click', () => { this.setViewMode('server'); if (this.isMobileLayout()) this.closeSidebar(); }); - + // Presets document.getElementById('view-presets').addEventListener('click', () => { document.getElementById('presets-modal').classList.remove('hidden'); }); - + document.getElementById('close-presets').addEventListener('click', () => { document.getElementById('presets-modal').classList.add('hidden'); }); - + // Preset buttons document.querySelectorAll('.preset-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -1906,9 +1906,9 @@ class ClaudeOrchestrator { document.getElementById('presets-modal').classList.add('hidden'); }); }); - + // Grid layout dropdown removed - using dynamic layout now - + // Settings const settingsToggle = document.getElementById('settings-toggle'); const closeSettingsPanel = () => { @@ -1936,7 +1936,7 @@ class ClaudeOrchestrator { } else { console.error('Settings toggle button not found!'); } - + document.getElementById('close-settings').addEventListener('click', () => { closeSettingsPanel(); }); @@ -1962,7 +1962,7 @@ class ClaudeOrchestrator { if (panel.contains(e.target)) return; closeSettingsPanel(); }); - + // Settings inputs document.getElementById('enable-notifications').addEventListener('change', (e) => { this.settings.notifications = e.target.checked; @@ -1971,12 +1971,12 @@ class ClaudeOrchestrator { this.notificationManager.requestPermission(); } }); - + document.getElementById('enable-sounds').addEventListener('change', (e) => { this.settings.sounds = e.target.checked; this.saveSettings(); }); - + document.getElementById('auto-scroll').addEventListener('change', (e) => { this.settings.autoScroll = e.target.checked; this.saveSettings(); @@ -1994,7 +1994,7 @@ class ClaudeOrchestrator { } } }); - + document.getElementById('theme-select').addEventListener('change', (e) => { this.settings.theme = e.target.value; this.saveSettings(); @@ -2048,6 +2048,7 @@ class ClaudeOrchestrator { // Settings UI helpers: search + section jump so the panel doesnโ€™t feel like an endless scroll. this.setupSettingsPanelNavigation(); this.setupDiagnosticsPanel(); + this.setupDependencySetupWizard(); const tasksThemeSelect = document.getElementById('tasks-theme-select'); if (tasksThemeSelect) { @@ -2306,7 +2307,7 @@ class ClaudeOrchestrator { document.getElementById('dismiss-git-notification').addEventListener('click', () => { document.getElementById('git-update-notification').classList.add('hidden'); }); - + // Workflow notification settings (server-persisted) const workflowNotifyMode = document.getElementById('workflow-notify-mode'); if (workflowNotifyMode) { @@ -2727,33 +2728,33 @@ class ClaudeOrchestrator { if (markReadBtn) { markReadBtn.addEventListener('click', () => this.notificationManager?.markAllAsRead?.()); } - + // Claude startup modal handlers (simplified) const cancelClaudeBtn = document.getElementById('cancel-claude-startup'); - + if (cancelClaudeBtn) { cancelClaudeBtn.addEventListener('click', () => { this.hideClaudeStartupModal(); }); } - + // Handle startup option button clicks document.addEventListener('click', (e) => { if (e.target.closest('.startup-option-btn')) { const btn = e.target.closest('.startup-option-btn'); const mode = btn.dataset.mode; - + // Check if modal YOLO is checked const modalYolo = document.getElementById('modal-yolo'); const skipPermissions = modalYolo ? modalYolo.checked : false; - + if (this.pendingClaudeSession) { this.startClaudeWithOptions(this.pendingClaudeSession, mode, skipPermissions); this.hideClaudeStartupModal(); } } }); - + // Handle window resize to fix blank terminals let resizeTimeout; window.addEventListener('resize', () => { @@ -2899,7 +2900,7 @@ class ClaudeOrchestrator { }); }); } - + setViewMode(mode, { persist = true } = {}) { const normalized = String(mode || '').toLowerCase(); if (!['all', 'claude', 'server'].includes(normalized)) return; @@ -2914,7 +2915,7 @@ class ClaudeOrchestrator { this.updateGlobalUserSetting('ui.terminals.viewMode', normalized); } } - + updateViewModeButtons() { const allBtn = document.getElementById('view-all'); const claudeBtn = document.getElementById('view-claude-only'); @@ -3544,21 +3545,21 @@ class ClaudeOrchestrator { return null; } - + matchesViewMode(sessionId) { if (this.viewMode === 'all') return true; - + const session = this.sessions.get(sessionId); const type = session?.type; - + if (this.viewMode === 'claude') { return type === 'claude' || type === 'codex' || /-(claude|codex)$/.test(String(sessionId || '')); } - + if (this.viewMode === 'server') { return type === 'server' || sessionId.includes('-server'); } - + return true; } @@ -3571,7 +3572,7 @@ class ClaudeOrchestrator { && this.matchesTierFilter(sessionId) && this.matchesWorkflowMode(sessionId); } - + handleInitialSessions(sessionStates) { console.log('Received initial sessions:', sessionStates); @@ -3588,7 +3589,7 @@ class ClaudeOrchestrator { this.sessions.clear(); this.sessionActivity.clear(); this.visibleTerminals.clear(); - + // Process sessions for (const [sessionId, state] of Object.entries(sessionStates)) { const sessionData = { @@ -3634,13 +3635,13 @@ class ClaudeOrchestrator { this.pruneIntentHaikuState(new Set(Object.keys(sessionStates))); this.lastSessionsWorkspaceId = currentWorkspaceId; - + // Hide loading message FIRST const loadingMessage = document.getElementById('loading-message'); if (loadingMessage) { loadingMessage.style.display = 'none'; } - + // Build sidebar this.buildSidebar(); @@ -4237,10 +4238,10 @@ class ClaudeOrchestrator { // Always ensure filter toggle exists and is updated FIRST this.ensureFilterToggleExists(); - + // Clear and rebuild the worktree list worktreeList.innerHTML = ''; - + // Group sessions by worktree and repository for mixed-repo support const worktrees = new Map(); @@ -4274,22 +4275,22 @@ class ClaudeOrchestrator { worktree.server = session; } } - + // Create sidebar items for (const [worktreeId, worktree] of worktrees) { // Check if worktree is active (has any session marked as active) const isActive = this.isWorktreeActive(worktreeId); - + // Skip inactive worktrees if filter is enabled if (this.showActiveOnly && !isActive) { continue; } - + // Check if any session in this worktree is visible. const claudeVisible = !!(worktree.claude && this.isSessionVisibleByWorktreeSelection(worktree.claude.sessionId, worktree.claude)); const serverVisible = !!(worktree.server && this.isSessionVisibleByWorktreeSelection(worktree.server.sessionId, worktree.server)); const isVisible = claudeVisible || serverVisible; - + const item = document.createElement('div'); // Only show visibility state, not activity state (activity filtering is handled separately) item.className = `worktree-item ${!isVisible ? 'hidden-terminal' : ''}`; @@ -4408,9 +4409,9 @@ class ClaudeOrchestrator { `; - + // Click handler is already attached via event delegation in setupEventListeners - + worktreeList.appendChild(item); } @@ -4791,21 +4792,21 @@ class ClaudeOrchestrator { const current = this.worktreeTags.get(worktreePath)?.readyForReview; return this.setWorktreeReadyForReview(worktreePath, !current); } - + ensureFilterToggleExists() { let filterToggle = document.getElementById('filter-toggle'); - + if (!filterToggle) { // Create the filter toggle element filterToggle = document.createElement('div'); filterToggle.className = 'filter-toggle'; filterToggle.id = 'filter-toggle'; - + // Insert it right before the worktree list const worktreeList = document.getElementById('worktree-list'); worktreeList.parentNode.insertBefore(filterToggle, worktreeList); } - + // Always update the button content const visibility = this.getUiVisibilityConfig().sidebar || {}; const showActiveFilter = visibility.activeFilter !== false; @@ -4838,7 +4839,7 @@ class ClaudeOrchestrator { if (status === 'error') return 'error'; return 'idle'; } - + isWorktreeActive(worktreeIdOrKey) { // Check if any session for this worktree has been marked as active. // For mixed-repo workspaces we may receive: @@ -4867,11 +4868,11 @@ class ClaudeOrchestrator { return false; } - + toggleActivityFilter() { this.showActiveOnly = !this.showActiveOnly; this.buildSidebar(); - + // Also update the main grid view to match the filter if (this.showActiveOnly) { this.showActiveWorktreesOnly(); @@ -4879,11 +4880,11 @@ class ClaudeOrchestrator { this.showAllTerminals(); } } - + showActiveWorktreesOnly() { // Clear visible terminals first this.visibleTerminals.clear(); - + // Add only active worktree sessions to visible set for (const [sessionId, session] of this.sessions) { const sessionWorktreeId = session.worktreeId || sessionId.split('-')[0]; @@ -4894,7 +4895,7 @@ class ClaudeOrchestrator { this.visibleTerminals.add(sessionId); } } - + // If no active sessions, show all if (this.visibleTerminals.size === 0) { this.showAllTerminals(); @@ -4903,7 +4904,7 @@ class ClaudeOrchestrator { this.buildSidebar(); } } - + resizeAllVisibleTerminals() { // Force resize all visible terminals to fit their containers this.activeView.forEach(sessionId => { @@ -5037,7 +5038,7 @@ class ClaudeOrchestrator { this.updateTerminalGrid(); this.buildSidebar(); } - + showWorktree(worktreeIdOrKey) { // Show terminals for this EXACT worktree key const claudeId = `${worktreeIdOrKey}-claude`; @@ -5049,17 +5050,17 @@ class ClaudeOrchestrator { this.updateTerminalGrid(); this.buildSidebar(); } - + showAllTerminals() { // Add all sessions to visible set for (const sessionId of this.sessions.keys()) { this.visibleTerminals.add(sessionId); } - + this.updateTerminalGrid(); this.buildSidebar(); } - + /** * Get the terminal grid container for the current tab */ @@ -5088,7 +5089,7 @@ class ClaudeOrchestrator { const allSessions = Array.from(this.sessions.keys()); this.renderTerminalsWithVisibility(allSessions); } - + renderTerminalsWithVisibility(sessionIds) { // Render all terminals but apply visibility using CSS (don't destroy DOM) this.activeView = sessionIds.filter(id => this.isSessionVisibleInCurrentView(id)); @@ -5232,18 +5233,18 @@ class ClaudeOrchestrator { this.resizeAllVisibleTerminals(); }, 200); } - + showClaudeOnly() { this.setViewMode('claude'); } - + showServersOnly() { this.setViewMode('server'); } - + applyPreset(preset) { this.visibleTerminals.clear(); - + switch (preset) { case 'all': this.showAllTerminals(); @@ -5277,9 +5278,9 @@ class ClaudeOrchestrator { break; } } - + // changeLayout method removed - using dynamic layout based on visible terminal count - + showTerminals(sessionIds) { // Legacy function - update visible set and refresh everything this.visibleTerminals.clear(); @@ -5291,31 +5292,31 @@ class ClaudeOrchestrator { this.updateTerminalGrid(); this.buildSidebar(); } - + renderTerminals(sessionIds) { // Core rendering function - just displays terminals without updating state this.activeView = sessionIds; const grid = this.getTerminalGrid(); - + // Sort sessionIds to ensure proper ordering: work1-claude, work1-server, work2-claude, work2-server, etc. const sortedSessionIds = sessionIds.slice().sort((a, b) => { // Extract worktree number const getWorkNum = (id) => parseInt(id.match(/work(\d+)/)?.[1] || 0); const numA = getWorkNum(a); const numB = getWorkNum(b); - + // First sort by worktree number if (numA !== numB) return numA - numB; - + // Then claude before server if (a.includes('claude') && b.includes('server')) return -1; if (a.includes('server') && b.includes('claude')) return 1; return 0; }); - + // Clear grid but don't destroy terminals grid.innerHTML = ''; - + // Create terminal elements for active view sortedSessionIds.forEach((sessionId) => { const session = this.sessions.get(sessionId); @@ -5324,7 +5325,7 @@ class ClaudeOrchestrator { grid.appendChild(wrapper); } }); - + // Now handle terminal instances sortedSessionIds.forEach((sessionId, index) => { const session = this.sessions.get(sessionId); @@ -5332,25 +5333,25 @@ class ClaudeOrchestrator { setTimeout(() => { const terminalEl = document.getElementById(this.getSessionDomId('terminal', sessionId)); if (!terminalEl) return; - + if (this.terminalManager.terminals.has(sessionId)) { // Re-attach existing terminal to the new element const term = this.terminalManager.terminals.get(sessionId); - + // Clear and re-open the terminal in the new element terminalEl.innerHTML = ''; term.open(terminalEl); - + // Force a resize and refresh this.terminalManager.fitTerminal(sessionId); - + // Force a screen refresh to show content term.refresh(0, term.rows - 1); } else { // Create new terminal only if it doesn't exist this.terminalManager.createTerminal(sessionId, session); } - + // Don't auto-start Claude - let user choose via modal or button }, 50 + (index * 25)); // Reduced stagger time } @@ -5576,41 +5577,41 @@ class ClaudeOrchestrator { getTicketMetaForSession(sessionId, sessionOverride = null) { const sid = String(sessionId || '').trim(); if (!sid) return null; - + const session = sessionOverride || this.sessions.get(sid); const recordIds = []; recordIds.push(`session:${sid}`); - + const worktreePath = session?.config?.cwd || session?.cwd || session?.worktreePath || null; if (worktreePath) recordIds.push(`worktree:${worktreePath}`); - + const prUrl = this.githubLinks.get(sid)?.pr || null; const prTaskId = prUrl ? this.getPRTaskIdFromUrl(prUrl) : null; if (prTaskId) recordIds.push(prTaskId); - + for (const id of recordIds) { const rec = this.taskRecords.get(id); if (!rec) continue; - + const ticketProvider = String(rec.ticketProvider || '').trim().toLowerCase(); const ticketCardId = String(rec.ticketCardId || '').trim(); const ticketCardUrl = String(rec.ticketCardUrl || '').trim(); const ticketTitle = String(rec.ticketTitle || '').trim(); - + if (!ticketTitle && !ticketCardId && !ticketCardUrl) continue; - + const rawUrl = ticketCardUrl || ((ticketProvider === 'trello' || !ticketProvider) && ticketCardId ? `https://trello.com/c/${ticketCardId}` : ''); const url = (rawUrl && /^https?:\/\//i.test(rawUrl)) ? rawUrl : ''; - + const label = ticketTitle || (ticketProvider && ticketCardId ? `${ticketProvider}:${ticketCardId}` : (ticketCardId ? ticketCardId : url)); const tooltipParts = [ ticketTitle || null, ticketProvider && ticketCardId ? `${ticketProvider}:${ticketCardId}` : null, url || null ].filter(Boolean); - + return { provider: ticketProvider || null, cardId: ticketCardId || null, @@ -5620,36 +5621,36 @@ class ClaudeOrchestrator { tooltip: tooltipParts.join(' โ€ข ') }; } - + return null; } - + updateTerminalTicketLabel(sessionId) { const sid = String(sessionId || '').trim(); if (!sid) return; - + const wrapper = this.getSessionWrapperElement(sid); if (!wrapper) return; - + const titleRow = wrapper.querySelector('.terminal-title'); if (!titleRow) return; - + const existing = titleRow.querySelector('.terminal-ticket'); const meta = this.getTicketMetaForSession(sid); - + if (!meta || !meta.label) { existing?.remove(); return; } - + const label = `๐Ÿงพ ${meta.label}`; const tooltip = meta.tooltip || meta.title || meta.label; const url = meta.url || ''; - + const wantsLink = !!url; const isLink = existing && existing.tagName && existing.tagName.toLowerCase() === 'a'; const isSpan = existing && existing.tagName && existing.tagName.toLowerCase() === 'span'; - + if (wantsLink) { let el = existing; if (!isLink) { @@ -5658,7 +5659,7 @@ class ClaudeOrchestrator { el.className = 'terminal-ticket'; el.target = '_blank'; el.rel = 'noopener noreferrer'; - + const branchEl = titleRow.querySelector('.terminal-branch'); if (branchEl && branchEl.parentElement === titleRow) { branchEl.insertAdjacentElement('afterend', el); @@ -5671,7 +5672,7 @@ class ClaudeOrchestrator { el.title = tooltip; return; } - + let el = existing; if (!isSpan) { existing?.remove(); @@ -5687,7 +5688,7 @@ class ClaudeOrchestrator { el.textContent = label; el.title = tooltip; } - + refreshBranchLabels() { try { for (const [sessionId, session] of this.sessions) { @@ -5699,7 +5700,7 @@ class ClaudeOrchestrator { } this.buildSidebar(); } - + createTerminalElement(sessionId, session) { const wrapper = document.createElement('div'); wrapper.className = 'terminal-wrapper'; @@ -5709,7 +5710,7 @@ class ClaudeOrchestrator { wrapper.addEventListener('mousedown', () => { this.lastInteractedSessionId = sessionId; }); - + const sessionType = String(session?.type || '').trim().toLowerCase(); const isAgentSession = sessionType === 'claude' || sessionType === 'codex'; const isServerSession = sessionType === 'server'; @@ -5819,7 +5820,7 @@ class ClaudeOrchestrator { return wrapper; } - + updateSessionStatus(sessionId, status) { const statusElement = document.getElementById(this.getSessionDomId('status', sessionId)); // Update session data @@ -5903,7 +5904,7 @@ class ClaudeOrchestrator { apply(status); } } - + // Update quick actions for agent sessions if (/-claude$|-codex$/.test(sessionId)) { // Clear any pending notification timer if agent goes busy again @@ -5935,11 +5936,11 @@ class ClaudeOrchestrator { }, 3000); } } - + // Update sidebar this.updateSidebarStatus(sessionId, status); } - + updateSidebarStatus(sessionId, status) { const session = this.sessions.get(sessionId); const key = this.getSessionWorktreeKey(sessionId, session); @@ -5996,7 +5997,7 @@ class ClaudeOrchestrator { } } } - + updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR) { const session = this.sessions.get(sessionId); if (session) { @@ -6007,9 +6008,9 @@ class ClaudeOrchestrator { if (defaultBranch) { session.defaultBranch = defaultBranch; } - + console.log(`Branch updated for ${sessionId}: ${branch}`, existingPR ? `(existing PR: ${existingPR})` : ''); - + // If there's an existing PR, add it to GitHub links automatically if (existingPR) { const links = this.githubLinks.get(sessionId) || {}; @@ -6019,17 +6020,17 @@ class ClaudeOrchestrator { this.maybeSchedulePrIntentRefresh(sessionId, existingPR); } } - + // Update terminal branch display this.updateTerminalBranchLabel(sessionId, branch || ''); - + // Update sidebar this.buildSidebar(); - + // Update GitHub buttons with new remote URL this.updateTerminalControls(sessionId); } - + // Server control methods toggleServer(sessionId, environment = 'development') { const status = this.serverStatuses.get(sessionId); @@ -6056,22 +6057,22 @@ class ClaudeOrchestrator { }); } } - + killServer(sessionId) { // Send force kill this.socket.emit('server-control', { sessionId, action: 'kill' }); this.serverStatuses.set(sessionId, 'idle'); - + // Update UI const button = document.getElementById(`server-toggle-${sessionId}`); if (button) { button.textContent = 'โ–ถ'; } - + this.updateSidebarStatus(sessionId, 'idle'); this.updateServerControls(sessionId); } - + playInHytopia(sessionId) { console.log(`[PLAY IN HYTOPIA] Session: ${sessionId}`); console.log('Available ports:', Array.from(this.serverPorts.entries())); @@ -6089,21 +6090,21 @@ class ClaudeOrchestrator { } return; } - + const serverUrl = `localhost:${port}`; const hytopiaUrl = `https://hytopia.com/play/?${serverUrl}`; - + console.log(`Opening Hytopia for ${sessionId} at ${hytopiaUrl}`); window.open(hytopiaUrl, '_blank'); } - + restoreBuildButton(sessionId) { // Find any button that might be building for this worktree const worktreeMatch = sessionId.match(/work(\d+)/); if (!worktreeMatch) return; - + const worktreeNum = worktreeMatch[1]; - + // Check both claude and server buttons for this worktree [`work${worktreeNum}-claude`, `work${worktreeNum}-server`].forEach(id => { const btn = this.buildingButtons?.get(id); @@ -6115,7 +6116,7 @@ class ClaudeOrchestrator { } }); } - + buildProduction(sessionId) { // Extract worktree number from sessionId (e.g., 'work1-claude' -> 1) const worktreeMatch = sessionId.match(/work(\d+)/); @@ -6124,10 +6125,10 @@ class ClaudeOrchestrator { this.showError('Failed to identify worktree for build'); return; } - + const worktreeNum = worktreeMatch[1]; console.log(`Building production ZIP for worktree ${worktreeNum}`); - + // Disable the build button and show loading state const wrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId)); const buildBtn = wrapper ? wrapper.querySelector('button[onclick*="buildProduction"]') : null; @@ -6136,26 +6137,26 @@ class ClaudeOrchestrator { buildBtn.innerHTML = ''; buildBtn.classList.add('building'); } - + // Store the button reference for later this.buildingButtons = this.buildingButtons || new Map(); this.buildingButtons.set(sessionId, buildBtn); - + // Emit socket event to trigger build on backend - this.socket.emit('build-production', { + this.socket.emit('build-production', { sessionId, - worktreeNum + worktreeNum }); } - + detectGitHubLinks(sessionId, data) { // Look for GitHub URLs with improved pattern matching const githubUrlPattern = /https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(?:\/(?!https:\/\/github\.com\/)[^\s\)\]\}\>\"\'\`]*)?/g; const matches = data.match(githubUrlPattern); - + if (matches) { const links = this.githubLinks.get(sessionId) || {}; - + matches.forEach(originalUrl => { // Clean up ANSI escape codes and other terminal artifacts let url = originalUrl @@ -6164,7 +6165,7 @@ class ClaudeOrchestrator { .replace(/\u001b\[[0-9;]*m/g, '') // Remove Unicode ANSI codes .replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove other control characters .trim(); - + // Remove common trailing punctuation that might be captured url = url.replace(/[,;.!?)\]}>'"`]*$/, ''); @@ -6173,7 +6174,7 @@ class ClaudeOrchestrator { if (secondUrlIndex > 0) { url = url.slice(0, secondUrlIndex); } - + // Validate URL format try { new URL(url); @@ -6181,7 +6182,7 @@ class ClaudeOrchestrator { console.warn('Invalid GitHub URL detected:', url); return; } - + // Categorize the URL if (url.includes('/pull/') && url.match(/\/pull\/\d+\/?$/)) { if (links.pr !== url) { @@ -6201,36 +6202,36 @@ class ClaudeOrchestrator { } } }); - + this.githubLinks.set(sessionId, links); this.updateTerminalControls(sessionId); } } - + clearGitHubLinks(sessionId) { this.githubLinks.delete(sessionId); this.githubLinkLogs.delete(sessionId); this.updateTerminalControls(sessionId); } - + copyLocalhostUrl(sessionId) { const port = this.serverPorts.get(sessionId); if (!port) { console.error('No port found for server', sessionId); return; } - + const url = `https://localhost:${port}`; navigator.clipboard.writeText(url).then(() => { console.log(`Copied ${url} to clipboard`); this.showNotification('Copied!', `${url} copied to clipboard`); }); } - + openHytopiaWebsite() { window.open('https://hytopia.com', '_blank'); } - + openPRLink(url) { try { // Validate the URL @@ -6242,17 +6243,17 @@ class ClaudeOrchestrator { this.showToast('Invalid PR URL', 'error'); } } - + getGitHubButtons(sessionId) { const links = this.githubLinks.get(sessionId) || {}; let buttons = ''; const visibility = this.getTerminalVisibilityConfig(); - + // Always show branch button (uses current session's git info) const session = this.sessions.get(sessionId); if (session && session.branch && session.branch !== 'master' && session.branch !== 'main') { const worktreeId = sessionId.split('-')[0]; - + // Use dynamic remote URL if available if (session.remoteUrl) { const encodeRef = (ref) => encodeURIComponent(String(ref || '').trim()); @@ -6261,7 +6262,7 @@ class ClaudeOrchestrator { // Use the actual default branch from git, fallback to 'main' if not available const defaultBranch = session.defaultBranch || 'main'; const compareUrl = `${session.remoteUrl}/compare/${encodeRef(defaultBranch)}...${branchRef}`; - + if (visibility.viewBranchOnGithub !== false) { buttons += ``; } @@ -6273,7 +6274,7 @@ class ClaudeOrchestrator { } } } - + // Show PR button if PR link detected if (links.pr) { const lastLogged = this.githubLinkLogs.get(sessionId); @@ -6288,17 +6289,17 @@ class ClaudeOrchestrator { buttons += ``; } } - + // Check for commit URLs if (links.commit) { if (visibility.advancedDiff !== false) { buttons += ``; } } - + return buttons; } - + updateTerminalControls(sessionId) { const wrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId)); if (!wrapper) return; @@ -6378,43 +6379,43 @@ class ClaudeOrchestrator { : ''; return [stopBtn, removeBtn].filter(Boolean).join('\n'); } - + updateServerStatus(sessionId, output) { // Check if server started - look for various startup messages - if (output.includes('Server started') || - output.includes('Listening on') || + if (output.includes('Server started') || + output.includes('Listening on') || output.includes('Server running') || output.includes('Started server') || output.includes('๐Ÿš€')) { this.serverStatuses.set(sessionId, 'running'); this.updateSidebarStatus(sessionId, 'running'); - + const button = document.getElementById(`server-toggle-${sessionId}`); if (button) { button.textContent = 'โน'; } - + this.updateServerControls(sessionId); const linkedClaude = this.getLinkedClaudeSessionIdForServer(sessionId); if (linkedClaude) this.updateTerminalControls(linkedClaude); } - + // Check if server stopped if (output.includes('Server stopped') || output.includes('exit')) { this.serverStatuses.set(sessionId, 'idle'); this.updateSidebarStatus(sessionId, 'idle'); - + const button = document.getElementById(`server-toggle-${sessionId}`); if (button) { button.textContent = 'โ–ถ'; } - + this.updateServerControls(sessionId); const linkedClaude = this.getLinkedClaudeSessionIdForServer(sessionId); if (linkedClaude) this.updateTerminalControls(linkedClaude); } } - + /** * Get dynamic launch options based on current workspace */ @@ -6485,14 +6486,14 @@ class ClaudeOrchestrator { // Use dynamic button system controlsDiv.innerHTML = this.getServerControlsHTML(sessionId); } - + handleServerError(sessionId, output) { const worktreeId = sessionId.split('-')[0]; - + // Update status this.serverStatuses.set(sessionId, 'error'); this.updateSidebarStatus(sessionId, 'error'); - + // Show notification this.notificationManager.handleNotification({ sessionId, @@ -6518,13 +6519,13 @@ class ClaudeOrchestrator { session.hasUserInput = false; this.updateSidebarStatus(sid, String(session.status || 'idle').trim().toLowerCase() || 'idle'); } - + sendTerminalInput(sessionId, data) { if (!this.socket || !this.socket.connected) { console.error('Not connected to server'); return; } - + // Mark session as active when user first provides input // But only for meaningful input (not just arrow keys, etc.) if (data.length > 0 && !data.match(/^[\x1b\x7f\r\n]/) && data.trim().length > 0) { @@ -6547,7 +6548,7 @@ class ClaudeOrchestrator { console.error('Failed to interrupt session', e); } } - + resizeTerminal(sessionId, cols, rows) { if (this.socket && this.socket.connected) { this.socket.emit('terminal-resize', { sessionId, cols, rows }); @@ -7522,7 +7523,7 @@ class ClaudeOrchestrator { } } } - + handleSessionRestart(sessionId) { console.log(`Session ${sessionId} restarted`); // Terminal will automatically reconnect and show new content @@ -7551,20 +7552,20 @@ class ClaudeOrchestrator { } } } - + restartClaudeSession(sessionId) { console.log(`Restarting Claude session: ${sessionId}`); - + if (this.socket && this.socket.connected) { this.socket.emit('restart-session', { sessionId }); - + // Update UI to show restarting this.updateSessionStatus(sessionId, 'restarting'); } else { this.showError('Not connected to server'); } } - + refreshTerminal(sessionId) { console.log('Refreshing terminal:', sessionId); const term = this.terminalManager.terminals.get(sessionId); @@ -7572,10 +7573,10 @@ class ClaudeOrchestrator { // Force fit and refresh this.terminalManager.fitTerminal(sessionId); term.refresh(0, term.rows - 1); - + // Also try scrolling to bottom to trigger redraw term.scrollToBottom(); - + // If still blank, re-attach to DOM const terminalEl = document.getElementById(this.getSessionDomId('terminal', sessionId)); if (terminalEl && terminalEl.children.length === 0) { @@ -7583,13 +7584,13 @@ class ClaudeOrchestrator { } } } - + updateConnectionStatus(connected) { const statusElement = document.getElementById('connection-status'); if (statusElement) { const dot = statusElement.querySelector('.status-dot'); const text = statusElement.querySelector('span:last-child'); - + if (connected) { dot.classList.remove('disconnected'); dot.classList.add('connected'); @@ -7601,12 +7602,12 @@ class ClaudeOrchestrator { } } } - + showError(message) { // For now, use alert. Could be improved with a toast notification alert(`Error: ${message}`); } - + showClaudeUpdateRequired(updateInfo) { // Create update banner const banner = document.createElement('div'); @@ -7621,10 +7622,10 @@ class ClaudeOrchestrator { `; - + // Add to top of page document.body.insertBefore(banner, document.body.firstChild); - + // Also show in console console.warn('Claude Update Required:', updateInfo); } @@ -7661,36 +7662,18 @@ class ClaudeOrchestrator { return; } this.lastNotificationTime[sessionId] = now; - + const worktreeId = sessionId.replace('-claude', ''); const session = this.sessions.get(sessionId); const branch = session ? session.branch : ''; - - // Create small toast notification - const toast = document.createElement('div'); - toast.className = 'ready-toast'; - toast.innerHTML = ` -
- โœ… - Claude ${worktreeId} ready ${branch ? `(${branch})` : ''} -
- `; - - // Add to page - document.body.appendChild(toast); - - // Remove after 3 seconds - setTimeout(() => { - if (toast.parentNode) { - toast.remove(); - } - }, 3000); - + + this.showToast(`Claude ${worktreeId} ready ${branch ? `(${branch})` : ''}`, 'success', { durationMs: 3000 }); + // Play notification sound if enabled if (this.settings.sounds) { this.playNotificationSound(); } - + // Browser notification if enabled if (this.settings.notifications && 'Notification' in window && Notification.permission === 'granted') { new Notification(`Claude ${worktreeId} Ready`, { @@ -7709,26 +7692,26 @@ class ClaudeOrchestrator { }); } } - + playNotificationSound() { // Create a simple notification sound const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); - + oscillator.connect(gainNode); gainNode.connect(audioContext.destination); - + oscillator.frequency.setValueAtTime(800, audioContext.currentTime); oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1); - + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); - + oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } - + loadSettings() { const stored = localStorage.getItem('claude-orchestrator-settings'); const defaults = { @@ -8501,11 +8484,11 @@ class ClaudeOrchestrator { globalNodeInput.value = nodeOptions.join(' '); globalArgsInput.value = gameArgs.join(' '); } - + saveSettings() { localStorage.setItem('claude-orchestrator-settings', JSON.stringify(this.settings)); } - + applyTheme() { const mode = this.settings.theme === 'light' ? 'light' : 'dark'; document.body.classList.toggle('light-theme', mode === 'light'); @@ -8683,7 +8666,7 @@ class ClaudeOrchestrator { } return normalized; } - + syncSettingsUI() { // Sync checkbox states with settings document.getElementById('enable-notifications').checked = this.settings.notifications; @@ -8702,29 +8685,29 @@ class ClaudeOrchestrator { const label = document.getElementById('skin-intensity-value'); if (label) label.textContent = `${v}%`; } - + // Sync user settings UI if loaded if (this.userSettings) { this.syncUserSettingsUI(); } } - + showCodeReviewDropdown(sessionId) { // Close any existing dropdowns document.querySelectorAll('.review-dropdown').forEach(dropdown => dropdown.remove()); - + // Get the terminal controls container const terminalWrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId)); const controlsContainer = terminalWrapper.querySelector('.terminal-controls'); - + // Create dropdown const dropdown = document.createElement('div'); dropdown.className = 'review-dropdown'; dropdown.innerHTML = this.buildReviewerDropdownHTML(sessionId); - + // Position and add to DOM controlsContainer.appendChild(dropdown); - + // Close dropdown when clicking outside const closeDropdown = (e) => { if (!dropdown.contains(e.target)) { @@ -8732,22 +8715,22 @@ class ClaudeOrchestrator { document.removeEventListener('click', closeDropdown); } }; - + // Add close listener after a short delay to prevent immediate closure setTimeout(() => { document.addEventListener('click', closeDropdown); }, 100); } - + buildReviewerDropdownHTML(requestingSessionId) { const availableReviewers = this.getAvailableReviewers(requestingSessionId); - + let html = `
Assign Code Review
`; - + if (availableReviewers.length === 0) { html += `
@@ -8767,19 +8750,19 @@ class ClaudeOrchestrator { `; }); } - + return html; } - + getAvailableReviewers(requestingSessionId) { const reviewers = []; - + for (const [sessionId, session] of this.sessions) { // Only include Claude sessions that are not the requesting session if (sessionId.includes('-claude') && sessionId !== requestingSessionId) { const worktreeNumber = sessionId.replace('-claude', '').replace('work', ''); const isActive = this.sessionActivity.get(sessionId) === 'active'; - + // Prefer active sessions, but include inactive ones as backup if (isActive || session.status === 'waiting') { reviewers.push({ @@ -8792,7 +8775,7 @@ class ClaudeOrchestrator { } } } - + // Sort by preference: active + ready first, then active + busy, then inactive reviewers.sort((a, b) => { if (a.isActive && !b.isActive) return -1; @@ -8801,48 +8784,48 @@ class ClaudeOrchestrator { if (a.status !== 'waiting' && b.status === 'waiting') return 1; return 0; }); - + return reviewers; } - + async assignCodeReview(requestingSessionId, reviewerSessionId) { // Close dropdown document.querySelectorAll('.review-dropdown').forEach(dropdown => dropdown.remove()); - + try { // Extract code/PR information from the requesting session const codeInfo = await this.extractCodeForReview(requestingSessionId); - + if (!codeInfo.hasContent) { this.showToast(`No code changes detected in Claude ${requestingSessionId.replace('work', '').replace('-claude', '')}`, 'warning'); return; } - + // Format review request const reviewRequest = this.formatReviewRequest(codeInfo, requestingSessionId); - + // Send to reviewer Claude this.sendTerminalInput(reviewerSessionId, reviewRequest); - + // Mark both sessions as active this.sessionActivity.set(reviewerSessionId, 'active'); this.buildSidebar(); - + // Show success message const requestingWorktree = requestingSessionId.replace('work', '').replace('-claude', ''); const reviewerWorktree = reviewerSessionId.replace('work', '').replace('-claude', ''); this.showToast(`Code review assigned: Claude ${requestingWorktree} โ†’ Claude ${reviewerWorktree}`, 'success'); - + } catch (error) { console.error('Error assigning code review:', error); this.showToast('Failed to assign code review', 'error'); } } - + async extractCodeForReview(sessionId) { // Get terminal content const terminalContent = this.terminalManager.getTerminalContent(sessionId); - + // Look for various types of code content const codePatterns = { prUrl: /https:\/\/github\.com\/[^\s]+\/pull\/\d+/g, @@ -8851,7 +8834,7 @@ class ClaudeOrchestrator { codeBlocks: /```[\s\S]*?```/g, bashCommands: /(?:git\s+(?:diff|log|show)|gh\s+pr)/g }; - + const extracted = { prUrls: [...(terminalContent.match(codePatterns.prUrl) || [])], gitDiffs: [...(terminalContent.match(codePatterns.gitDiff) || [])], @@ -8859,20 +8842,20 @@ class ClaudeOrchestrator { recentCommands: this.extractRecentCommands(terminalContent), hasContent: false }; - + // Determine if there's reviewable content - extracted.hasContent = extracted.prUrls.length > 0 || - extracted.gitDiffs.length > 0 || + extracted.hasContent = extracted.prUrls.length > 0 || + extracted.gitDiffs.length > 0 || extracted.codeBlocks.length > 0 || extracted.recentCommands.some(cmd => cmd.includes('git') || cmd.includes('gh pr')); - + return extracted; } - + extractRecentCommands(terminalContent) { const lines = terminalContent.split('\n'); const commands = []; - + // Look for command patterns (simple approach) for (let i = lines.length - 1; i >= 0 && commands.length < 10; i--) { const line = lines[i].trim(); @@ -8880,15 +8863,15 @@ class ClaudeOrchestrator { commands.unshift(line); } } - + return commands; } - + formatReviewRequest(codeInfo, requestingSessionId) { const requestingWorktree = requestingSessionId.replace('work', '').replace('-claude', ''); - + let request = `Please review the code from Claude ${requestingWorktree}:\n\n`; - + if (codeInfo.prUrls.length > 0) { request += `**Pull Request(s):**\n`; codeInfo.prUrls.forEach(url => { @@ -8900,19 +8883,19 @@ class ClaudeOrchestrator { request += `- Suggestions for improvement\n`; request += `- Architecture and design patterns\n\n`; } - + if (codeInfo.gitDiffs.length > 0) { request += `**Git Diff:**\n\`\`\`diff\n`; request += codeInfo.gitDiffs.slice(0, 2).join('\n'); // Limit to first 2 diffs request += `\n\`\`\`\n\n`; } - + if (codeInfo.codeBlocks.length > 0) { request += `**Code Changes:**\n`; request += codeInfo.codeBlocks.slice(0, 3).join('\n\n'); // Limit to first 3 blocks request += `\n\n`; } - + if (codeInfo.recentCommands.length > 0) { request += `**Recent Commands:**\n`; codeInfo.recentCommands.forEach(cmd => { @@ -8920,9 +8903,9 @@ class ClaudeOrchestrator { }); request += `\n`; } - + request += `Please provide a thorough code review with specific feedback and suggestions.\n`; - + return request; } @@ -9250,75 +9233,1500 @@ class ClaudeOrchestrator { throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`)); } - const diagnostics = data?.diagnostics; - if (diagnostics && typeof diagnostics === 'object') { - state.firstRun = diagnostics; - renderRepairActions(state.firstRun); - } else { - await refreshFirstRun(); + const diagnostics = data?.diagnostics; + if (diagnostics && typeof diagnostics === 'object') { + state.firstRun = diagnostics; + renderRepairActions(state.firstRun); + } else { + await refreshFirstRun(); + } + if (!state.base) await refreshBase(); + await refreshInstallWizard().catch(() => {}); + render(state.base, state.firstRun, state.wizard); + + const appliedCount = Number(data?.appliedCount || 0); + const failedCount = Number(data?.failedCount || 0); + const skippedManualCount = Number(data?.skippedManualCount || 0); + if (failedCount > 0) { + this.showToast?.(`Auto-fix applied ${appliedCount}, failed ${failedCount}`, 'warning'); + } else { + const tail = skippedManualCount > 0 ? `, ${skippedManualCount} manual step(s) left` : ''; + this.showToast?.(`Auto-fix applied ${appliedCount}${tail}`, 'success'); + } + if (statusEl) statusEl.textContent = 'Safe auto-fix completed'; + } catch (error) { + this.showToast?.(`Safe auto-fix failed: ${String(error?.message || error)}`, 'error'); + if (statusEl) statusEl.textContent = ''; + } finally { + btnRepairSafe.disabled = false; + } + }); + repairEl?.addEventListener('click', async (event) => { + const target = event.target.closest('[data-diagnostics-repair]'); + if (!target) return; + const action = String(target.getAttribute('data-diagnostics-repair') || '').trim(); + if (!action) return; + target.disabled = true; + if (statusEl) statusEl.textContent = `Running repair: ${action}โ€ฆ`; + try { + const res = await fetch('/api/diagnostics/first-run/repair', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action }) + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || data?.ok === false) { + throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`)); + } + const repair = data?.repair || {}; + if (repair.manual) { + this.showToast?.(String(repair?.message || 'Manual action required'), 'warning'); + } else { + this.showToast?.(String(repair?.message || 'Repair completed'), 'success'); + } + if (data?.diagnostics) { + state.firstRun = data.diagnostics; + renderRepairActions(state.firstRun); + } else { + await refreshFirstRun(); + } + if (!state.base) await refreshBase(); + await refreshInstallWizard().catch(() => {}); + render(state.base, state.firstRun, state.wizard); + if (statusEl) statusEl.textContent = `Repair completed: ${action}`; + } catch (error) { + this.showToast?.(`Repair failed: ${String(error?.message || error)}`, 'error'); + if (statusEl) statusEl.textContent = ''; + } finally { + target.disabled = false; + } + }); + } + + openDiagnosticsPanel({ refresh = true } = {}) { + try { + document.getElementById('settings-panel')?.classList?.remove?.('hidden'); + setTimeout(() => { + try { + document.getElementById('diagnostics-output')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); + } catch { + // ignore + } + if (!refresh) return; + if (typeof this.refreshDiagnosticsPanel === 'function') { + this.refreshDiagnosticsPanel(); + return; + } + try { + document.getElementById('diagnostics-refresh')?.click?.(); + } catch { + // ignore + } + }, 50); + } catch { + // ignore + } + } + + setupDependencySetupWizard() { + const modal = document.getElementById('dependency-setup-modal'); + const openBtn = document.getElementById('dependency-setup-open'); + const summaryEl = document.getElementById('dependency-setup-summary'); + const listEl = document.getElementById('dependency-setup-list'); + const closeBtn = document.getElementById('dependency-setup-close'); + if (!modal || !summaryEl || !listEl) return; + const body = document.body; + const isWindowsHost = (() => { + try { + const platform = String(navigator?.platform || '').toLowerCase(); + const userAgent = String(navigator?.userAgent || '').toLowerCase(); + return platform.includes('win') || userAgent.includes('windows'); + } catch { + return false; + } + })(); + + const setBootstrapPending = (pending) => { + if (pending) { + if (!isWindowsHost) return; + body?.classList?.add?.('dependency-onboarding-booting'); + body?.classList?.remove?.('dependency-onboarding-active'); + return; + } + body?.classList?.remove?.('dependency-onboarding-booting'); + }; + if (!isWindowsHost) { + setBootstrapPending(false); + return; + } + + const dismissKey = 'orchestrator-dependency-setup-dismissed-v3'; + const completedKey = 'orchestrator-dependency-onboarding-completed-v2'; + const progressKey = 'orchestrator-dependency-onboarding-progress-v2'; + const skippedStepsKey = 'orchestrator-dependency-onboarding-skipped-v1'; + const setupStateUrl = '/api/setup-actions/state'; + const readLegacyBool = (key) => { + try { + return localStorage.getItem(key) === 'true'; + } catch { + return false; + } + }; + const readLegacyStep = () => { + try { + const raw = Number.parseInt(String(localStorage.getItem(progressKey) || ''), 10); + if (Number.isFinite(raw) && raw >= 0) return raw; + return 0; + } catch { + return 0; + } + }; + const readLegacySkippedActionIds = () => { + try { + const raw = localStorage.getItem(skippedStepsKey); + if (!raw) return []; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return []; + const seen = new Set(); + return parsed + .map((value) => String(value || '').trim()) + .filter((value) => { + if (!value || seen.has(value)) return false; + seen.add(value); + return true; + }); + } catch { + return []; + } + }; + const normalizeSetupState = (value) => { + const currentStepRaw = Number.parseInt(String(value?.currentStep ?? 0), 10); + const skippedActionIds = Array.isArray(value?.skippedActionIds) ? value.skippedActionIds : []; + const seen = new Set(); + return { + dismissed: value?.dismissed === true, + completed: value?.completed === true, + currentStep: Number.isFinite(currentStepRaw) && currentStepRaw >= 0 ? currentStepRaw : 0, + skippedActionIds: skippedActionIds + .map((entry) => String(entry || '').trim()) + .filter((entry) => { + if (!entry || seen.has(entry)) return false; + seen.add(entry); + return true; + }) + }; + }; + const readBootstrapSetupState = () => { + try { + const bootstrapState = window.__ORCHESTRATOR_SETUP_STATE__; + if (bootstrapState && typeof bootstrapState === 'object') { + return normalizeSetupState(bootstrapState); + } + } catch { + // ignore and fall back to legacy/local state + } + return normalizeSetupState({ + dismissed: readLegacyBool(dismissKey), + completed: readLegacyBool(completedKey), + currentStep: readLegacyStep(), + skippedActionIds: readLegacySkippedActionIds() + }); + }; + const persistedSetupState = { + loaded: false, + loadPromise: null, + savePromise: Promise.resolve(), + current: readBootstrapSetupState() + }; + const syncLegacySetupState = () => { + const current = persistedSetupState.current || normalizeSetupState({}); + try { + if (current.dismissed) localStorage.setItem(dismissKey, 'true'); + else localStorage.removeItem(dismissKey); + if (current.completed) localStorage.setItem(completedKey, 'true'); + else localStorage.removeItem(completedKey); + localStorage.setItem(progressKey, String(Math.max(0, Number(current.currentStep) || 0))); + if (Array.isArray(current.skippedActionIds) && current.skippedActionIds.length > 0) { + localStorage.setItem(skippedStepsKey, JSON.stringify(current.skippedActionIds)); + } else { + localStorage.removeItem(skippedStepsKey); + } + } catch { + // ignore + } + }; + const getPersistedSetupState = () => ({ + ...(persistedSetupState.current || normalizeSetupState({})), + skippedActionIds: Array.isArray(persistedSetupState.current?.skippedActionIds) + ? [...persistedSetupState.current.skippedActionIds] + : [] + }); + const applyPersistedSetupState = (value) => { + persistedSetupState.current = normalizeSetupState(value || {}); + syncLegacySetupState(); + return getPersistedSetupState(); + }; + const loadPersistedSetupState = async ({ force = false } = {}) => { + if (persistedSetupState.loaded && !force) return getPersistedSetupState(); + if (persistedSetupState.loadPromise && !force) return persistedSetupState.loadPromise; + persistedSetupState.loadPromise = (async () => { + try { + const res = await fetch(setupStateUrl); + const data = await res.json().catch(() => ({})); + if (res.ok && data?.ok !== false) { + applyPersistedSetupState(data?.state || {}); + } + } catch { + // ignore and keep local fallback state + } finally { + persistedSetupState.loaded = true; + persistedSetupState.loadPromise = null; + } + return getPersistedSetupState(); + })(); + return persistedSetupState.loadPromise; + }; + const savePersistedSetupState = async (patch = {}) => { + applyPersistedSetupState({ + ...(persistedSetupState.current || normalizeSetupState({})), + ...((patch && typeof patch === 'object') ? patch : {}) + }); + persistedSetupState.savePromise = persistedSetupState.savePromise + .catch(() => null) + .then(async () => { + try { + const res = await fetch(setupStateUrl, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch || {}) + }); + const data = await res.json().catch(() => ({})); + if (res.ok && data?.ok !== false) { + applyPersistedSetupState(data?.state || {}); + } + } catch { + // ignore and keep local state + } + return getPersistedSetupState(); + }); + return persistedSetupState.savePromise; + }; + setBootstrapPending(true); + + const state = { + loading: false, + diagnostics: null, + actions: [], + currentStep: 0, + showWelcome: true, + skippedActionIds: new Set(), + actionRuns: new Map(), + actionRunPollers: new Map(), + gitIdentity: { + name: '', + email: '' + }, + gitIdentityHelpVisible: false, + startupPending: true + }; + + const readDismissed = () => getPersistedSetupState().dismissed === true; + + const writeDismissed = (value) => { + const next = value === true; + if (readDismissed() === next) return; + void savePersistedSetupState({ dismissed: next }); + }; + + const readCompleted = () => getPersistedSetupState().completed === true; + + const writeCompleted = (value) => { + const next = value === true; + if (readCompleted() === next) return; + void savePersistedSetupState({ completed: next }); + }; + + const readSavedStep = () => Math.max(0, Number(getPersistedSetupState().currentStep) || 0); + + const writeSavedStep = (step) => { + const next = Math.max(0, Number(step) || 0); + if (readSavedStep() === next) return; + void savePersistedSetupState({ currentStep: next }); + }; + + const readSkippedStepIds = () => new Set(getPersistedSetupState().skippedActionIds); + + const writeSkippedStepIds = () => { + const next = Array.from(state.skippedActionIds || []) + .map((value) => String(value || '').trim()) + .filter(Boolean); + const prev = getPersistedSetupState().skippedActionIds || []; + if (next.length === prev.length && next.every((value, index) => value === prev[index])) return; + void savePersistedSetupState({ skippedActionIds: next }); + }; + + const setStepSkipped = (actionId, skipped) => { + const id = String(actionId || '').trim(); + if (!id) return; + if (skipped) state.skippedActionIds.add(id); + else state.skippedActionIds.delete(id); + writeSkippedStepIds(); + }; + + const toToolMap = (diagnostics) => { + const map = new Map(); + const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : []; + tools.forEach((tool) => { + const id = String(tool?.id || '').trim(); + if (!id) return; + map.set(id, !!tool?.ok); + }); + return map; + }; + + const getToolResult = (diagnostics, toolId) => { + const id = String(toolId || '').trim(); + if (!id) return null; + const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : []; + return tools.find((tool) => String(tool?.id || '').trim() === id) || null; + }; + + const parseGitIdentityVersion = (value) => { + const raw = String(value || '').trim(); + if (!raw) return { name: '', email: '' }; + const pair = raw.match(/^(.*)\s<([^<>]+)>$/); + if (pair?.[1] && pair?.[2]) { + return { + name: String(pair[1] || '').trim(), + email: String(pair[2] || '').trim() + }; + } + if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) { + return { name: '', email: raw }; + } + return { name: raw, email: '' }; + }; + + const stripAnsiText = (value) => String(value || '').replace(/\u001b\[[0-9;]*m/g, ''); + + const collectRunOutputLines = (runInfo, { limit = 25 } = {}) => { + const lines = Array.isArray(runInfo?.output) + ? runInfo.output + .map((entry) => stripAnsiText(String(entry?.line || '')).trim()) + .filter(Boolean) + : []; + if (!Number.isFinite(limit) || limit <= 0) return lines; + return lines.slice(-Math.max(1, Number(limit) || 1)); + }; + + const extractGithubLoginInfo = (lines = []) => { + const fallbackUrl = 'https://github.com/login/device'; + let link = fallbackUrl; + let code = ''; + let sawDeviceHint = false; + + (Array.isArray(lines) ? lines : []).forEach((lineRaw) => { + const line = String(lineRaw || '').trim(); + if (!line) return; + + if (/one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i.test(line)) { + sawDeviceHint = true; + } + + const linkMatch = line.match(/https:\/\/github\.com\/login\/device(?:\S*)?/i); + if (linkMatch?.[0]) link = linkMatch[0].trim(); + + const codeMatch = line.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i); + if (codeMatch?.[1]) code = String(codeMatch[1]).toUpperCase(); + }); + + return { + link, + code, + sawDeviceHint + }; + }; + + const hydrateGitIdentityDraft = (diagnostics) => { + const gitIdentityTool = getToolResult(diagnostics, 'gitIdentity'); + const parsed = parseGitIdentityVersion(String(gitIdentityTool?.version || '')); + if (!state.gitIdentity.name && parsed.name) { + state.gitIdentity.name = parsed.name; + } + if (!state.gitIdentity.email && parsed.email) { + state.gitIdentity.email = parsed.email; + } + }; + + const getRequirementState = (toolsMap) => { + const gitOk = !!toolsMap.get('git'); + const claudeOk = !!toolsMap.get('claude'); + const codexOk = !!toolsMap.get('codex'); + const hasAgentCli = claudeOk || codexOk; + const coreReady = gitOk && hasAgentCli; + const missingCore = []; + if (!gitOk) missingCore.push('git'); + if (!hasAgentCli) missingCore.push('agent-cli'); + return { + gitOk, + claudeOk, + codexOk, + hasAgentCli, + coreReady, + missingCore + }; + }; + + const isActionComplete = (actionId, toolsMap) => { + switch (String(actionId || '').trim()) { + case 'install-git': + return !!toolsMap.get('git'); + case 'configure-git-identity': + return !!toolsMap.get('gitIdentity'); + case 'install-node': + return !!toolsMap.get('node') && !!toolsMap.get('npm'); + case 'install-gh': + return !!toolsMap.get('gh'); + case 'gh-login': + return !!toolsMap.get('ghAuth'); + case 'install-claude': + return !!toolsMap.get('claude'); + case 'install-codex': + return !!toolsMap.get('codex'); + default: + return false; + } + }; + + const getActionLevelText = (level) => { + if (level === 'required') return 'Required'; + if (level === 'optional') return 'Optional'; + if (level === 'core-option') return 'Core option'; + return 'Recommended'; + }; + + const getActionLevelClass = (level) => { + if (level === 'optional') return 'level-optional'; + return level === 'recommended' ? 'level-recommended' : 'level-required'; + }; + + const getActionStatusText = (actionId, done) => { + const id = String(actionId || '').trim(); + if (id === 'gh-login') return done ? 'Logged in' : 'Not logged in'; + if (id === 'configure-git-identity') return done ? 'Configured' : 'Not configured'; + return done ? 'Installed' : 'Missing'; + }; + + const getResolvedSteps = () => { + const toolsMap = toToolMap(state.diagnostics); + const actions = Array.isArray(state.actions) ? state.actions : []; + return actions.map((action) => { + const id = String(action?.id || '').trim(); + const level = getActionLevel(id); + const done = isActionComplete(id, toolsMap); + return { + ...action, + id, + level, + optional: action?.optional === true || level === 'optional', + done, + levelText: getActionLevelText(level), + levelClass: getActionLevelClass(level), + statusText: getActionStatusText(id, done), + statusClass: done ? 'status-ok' : 'status-missing', + runSupported: action?.runSupported !== false + }; + }); + }; + + const syncSkippedSteps = (steps) => { + if (!(state.skippedActionIds instanceof Set)) { + state.skippedActionIds = new Set(); + } + const validSkippedIds = new Set( + (Array.isArray(steps) ? steps : []) + .filter((step) => { + const id = String(step?.id || '').trim(); + return !!id && step?.optional && !step?.done; + }) + .map((step) => String(step?.id || '').trim()) + ); + let changed = false; + for (const id of Array.from(state.skippedActionIds)) { + if (!validSkippedIds.has(id)) { + state.skippedActionIds.delete(id); + changed = true; + } + } + if (changed) writeSkippedStepIds(); + }; + + const isOnboardingLocked = () => { + if (!isWindowsHost) return false; + if (readCompleted()) return false; + if (!Array.isArray(state.actions) || state.actions.length === 0) return false; + const toolsMap = toToolMap(state.diagnostics); + const req = getRequirementState(toolsMap); + return !req?.coreReady; + }; + + const applyOnboardingLockUI = () => { + const locked = isOnboardingLocked(); + if (closeBtn) { + closeBtn.disabled = locked; + closeBtn.style.visibility = locked ? 'hidden' : ''; + } + modal.setAttribute('data-onboarding-locked', locked ? 'true' : 'false'); + return locked; + }; + + const setCurrentStep = (nextStep, { persist = true } = {}) => { + const previousStep = state.currentStep; + const maxStep = Math.max(0, (Array.isArray(state.actions) ? state.actions.length : 0) - 1); + const parsed = Number.parseInt(String(nextStep), 10); + const safe = Number.isFinite(parsed) ? parsed : 0; + state.currentStep = Math.max(0, Math.min(safe, maxStep)); + if (state.currentStep !== previousStep) { + state.gitIdentityHelpVisible = false; + } + if (persist) writeSavedStep(state.currentStep); + return state.currentStep; + }; + + const getActionLevel = (actionId) => { + const id = String(actionId || '').trim(); + if (id === 'install-git') return 'required'; + if (id === 'configure-git-identity') return 'optional'; + if (id === 'install-gh' || id === 'gh-login') return 'optional'; + if (id === 'install-claude') return 'optional'; + if (id === 'install-codex') return 'optional'; + return 'recommended'; + }; + + const buildStepIconSvg = (iconMarkup) => ( + `` + ); + + const stepIconSvgByActionId = Object.freeze({ + 'install-git': buildStepIconSvg(''), + 'configure-git-identity': buildStepIconSvg(''), + 'install-node': buildStepIconSvg(''), + 'install-gh': buildStepIconSvg(''), + 'gh-login': buildStepIconSvg(''), + 'install-claude': buildStepIconSvg(''), + 'install-codex': buildStepIconSvg('') + }); + + const getStepIconSvg = (actionId) => { + const id = String(actionId || '').trim(); + return stepIconSvgByActionId[id] + || buildStepIconSvg(''); + }; + + const render = () => { + const toolsMap = toToolMap(state.diagnostics); + const req = getRequirementState(toolsMap); + const steps = getResolvedSteps(); + syncSkippedSteps(steps); + if (!steps.length) { + summaryEl.textContent = 'No setup actions are available for this platform.'; + listEl.innerHTML = '
No setup actions are available for this platform.
'; + return { req, steps, current: null }; + } + + setCurrentStep(state.currentStep, { persist: false }); + const current = steps[state.currentStep]; + const stepNo = state.currentStep + 1; + const totalSteps = steps.length; + const detectedCount = steps.filter((step) => step.done).length; + const doneRatio = totalSteps > 0 ? Math.round((detectedCount / totalSteps) * 100) : 0; + const missingCore = []; + if (!req.gitOk) missingCore.push('Git'); + if (!req.hasAgentCli) missingCore.push('Claude Code or Codex CLI'); + + if (state.showWelcome) { + summaryEl.textContent = ''; + listEl.innerHTML = ` +
+

Letโ€™s get you ready in a minute.

+

+ Weโ€™ll check your system and install whatโ€™s needed. + Optional tools can be skipped. +

+
+ +
+
`; + return { req, steps, current }; + } + + summaryEl.textContent = ''; + + const currentId = String(current?.id || '').trim(); + const currentStepIconSvg = getStepIconSvg(currentId); + const currentTitle = this.escapeHtml(String(current?.title || currentId || 'Setup action')); + const currentDesc = this.escapeHtml(String(current?.description || '')); + const commandRaw = String(current?.command || ''); + const runInfo = state.actionRuns.get(currentId) || null; + const runStatus = String(runInfo?.status || '').trim().toLowerCase(); + const isRunning = runStatus === 'running'; + const isVerifying = runStatus === 'verifying'; + const isFinalizing = runStatus === 'success' || runStatus === 'completed'; + const isRunBusy = isRunning || isVerifying || isFinalizing; + const isGitIdentityStep = currentId === 'configure-git-identity'; + const runOutputAll = collectRunOutputLines(runInfo, { limit: 160 }); + const runOutput = runOutputAll.slice(-8); + const runOutputText = this.escapeHtml(runOutput.join('\n')); + const shouldShowInstallerOutput = currentId !== 'gh-login' && !isGitIdentityStep && ( + runOutput.length > 0 || + isRunBusy || + runStatus === 'failed' || + runStatus === 'needs-attention' + ); + const installerOutputText = runOutput.length + ? runOutputText + : this.escapeHtml( + isRunning + ? 'Installer started. Waiting for output...' + : (isVerifying + ? 'Installer finished. Verifying dependency...' + : 'No installer output captured yet.') + ); + const githubDeviceUrl = 'https://github.com/login/device'; + const ghInstalled = !!toolsMap.get('gh'); + const ghLoggedIn = !!toolsMap.get('ghAuth'); + const ghLoginRunInfo = state.actionRuns.get('gh-login') || null; + const ghLoginRunStatus = String(ghLoginRunInfo?.status || '').trim().toLowerCase(); + const ghLoginIsRunning = ghLoginRunStatus === 'running'; + const ghLoginIsVerifying = ghLoginRunStatus === 'verifying'; + const ghLoginIsFinalizing = ghLoginRunStatus === 'success' || ghLoginRunStatus === 'completed'; + const ghLoginIsBusy = ghLoginIsRunning || ghLoginIsVerifying || ghLoginIsFinalizing; + const ghLoginOutputAll = collectRunOutputLines(ghLoginRunInfo, { limit: 160 }); + const ghLoginInfo = extractGithubLoginInfo(ghLoginOutputAll); + const ghLoginLink = String(ghLoginRunInfo?.ghDeviceUrl || ghLoginInfo.link || githubDeviceUrl).trim() || githubDeviceUrl; + const ghLoginCode = String(ghLoginRunInfo?.ghDeviceCode || ghLoginInfo.code || '').trim().toUpperCase(); + const ghLoginHasSignal = !!( + ghLoginRunInfo?.ghHasDeviceHint + || ghLoginInfo.sawDeviceHint + || ghLoginCode + || ghLoginLink !== githubDeviceUrl + ); + const ghLoginUiPhase = (() => { + if (!ghInstalled || ghLoggedIn) return 'none'; + if (!ghLoginRunInfo) return 'start'; + if (ghLoginCode) return 'code'; + if (ghLoginIsBusy) return 'wait-code'; + return 'retry'; + })(); + const ghLoginInlineStatusText = (() => { + if (!ghInstalled) return 'Install GitHub CLI first'; + if (ghLoggedIn) return 'Logged in'; + if (ghLoginIsFinalizing) return 'Finalizing login'; + if (ghLoginIsRunning) return 'Signing in'; + if (ghLoginIsVerifying) return 'Checking login'; + return 'Not logged in'; + })(); + const ghLoginInlineStatusClass = ghLoggedIn + ? 'status-ok' + : ((ghLoginIsBusy || ghLoginRunStatus === 'needs-attention') ? 'status-pending' : 'status-missing'); + const ghLoginInlineRunLabel = (() => { + if (ghLoggedIn) return 'Logged in'; + if (ghLoginIsFinalizing) return 'Finalizing...'; + if (ghLoginIsBusy) return 'Waiting...'; + return 'Start login'; + })(); + const ghLoginInlineRunDisabled = !ghInstalled || ghLoggedIn || ghLoginIsBusy; + const showInlineGhLogin = currentId === 'install-gh' && ghInstalled; + const isGhLoginStep = currentId === 'gh-login'; + const codexNeedsNode = currentId === 'install-codex' && !(toolsMap.get('node') && toolsMap.get('npm')); + const gitIdentityName = this.escapeHtml(String(state.gitIdentity?.name || '')); + const gitIdentityEmail = this.escapeHtml(String(state.gitIdentity?.email || '')); + const showRunButton = current?.runSupported !== false && !isGitIdentityStep && !(isGhLoginStep && current?.done); + const runDisabled = !!current?.done || runStatus === 'verified' || isRunBusy || codexNeedsNode; + const runLabel = (() => { + if (current?.done || runStatus === 'verified') { + if (currentId === 'gh-login') return 'Logged in'; + if (currentId === 'configure-git-identity') return 'Configured'; + return 'Installed'; + } + if (isFinalizing) return 'Finalizing...'; + if (isRunBusy) return isGhLoginStep ? 'Waiting...' : 'Running...'; + if (currentId === 'gh-login') return 'Start login'; + return 'Run step'; + })(); + const baseStatusText = String(current?.statusText || (current?.done ? 'Installed' : 'Missing')); + const statusText = (() => { + if (runStatus === 'verified') return baseStatusText; + if (isFinalizing) return isGhLoginStep ? 'Finalizing login' : 'Finalizing'; + if (isRunning) return isGhLoginStep ? 'Signing in' : (isGitIdentityStep ? 'Saving' : 'Installing'); + if (isVerifying) return isGhLoginStep ? 'Checking login' : (isGitIdentityStep ? 'Checking' : 'Verifying'); + if (runStatus === 'failed') return isGhLoginStep ? 'Login failed' : (isGitIdentityStep ? 'Save failed' : 'Failed'); + return baseStatusText; + })(); + const statusClass = current?.done || runStatus === 'verified' + ? 'status-ok' + : ((isRunning || isVerifying || isFinalizing) ? 'status-pending' : (runStatus === 'failed' ? 'status-missing' : (current?.statusClass || 'status-missing'))); + const canAdvance = !!current?.done || !!current?.optional; + const nextLabel = !canAdvance + ? 'Complete this step first' + : (!current?.done && current?.optional + ? 'Skip' + : (stepNo >= totalSteps ? 'Finish onboarding' : 'Next step')); + + listEl.innerHTML = ` +
+ ${steps.map((step, idx) => { + const isActive = idx === state.currentStep; + const actionId = String(step?.id || '').trim(); + const isSkipped = state.skippedActionIds.has(actionId); + const isDone = step.done || isSkipped; + const isPast = idx < state.currentStep; + const isFuture = idx > state.currentStep; + let statusClass = 'stepper-upcoming'; + if (isActive) { + statusClass = 'stepper-active'; + } else if (isPast && isDone) { + statusClass = 'stepper-done'; + } else { + statusClass = 'stepper-upcoming'; + } + const stepStateLabel = isActive + ? 'Current step' + : (isPast && isDone ? 'Completed' : (isFuture ? 'Upcoming' : 'Pending')); + return ` +
+
+ ${isActive ? `Step ${stepNo}` : ''} +
+
+
+ `; + }).join('')} +
+ +
+
+ ${currentStepIconSvg} +
+
+

${currentTitle}

+ +
+ ${current?.done ? '' : ''} +

${currentDesc} ${statusText ? `(${statusText})` : ''}

+
+ + ${isGitIdentityStep ? ` +
+
+ + + +
+
+ ` : ''} + + ${showInlineGhLogin ? ` + + ` : ''} + + ${shouldShowInstallerOutput ? ` +
+
${installerOutputText}
+
+ ` : ''} + +
+ ${showRunButton ? `` : ''} + ${!isGhLoginStep && !isGitIdentityStep ? `` : ''} +
+
+
+ +
+ + +
`; + + return { req, steps, current }; + }; + + const closeModal = ({ force = false } = {}) => { + const locked = applyOnboardingLockUI(); + if (!force && locked) { + openModal(); + return false; + } + if (!force && !readCompleted()) { + writeDismissed(true); + } + modal.classList.add('hidden'); + body?.classList?.remove?.('dependency-onboarding-active'); + setBootstrapPending(false); + return true; + }; + const openModal = ({ showWelcome = null, allowDuringStartup = false } = {}) => { + if (readCompleted()) { + closeModal({ force: true }); + return false; + } + if (state.startupPending && !allowDuringStartup) { + return false; + } + const wasHidden = modal.classList.contains('hidden'); + modal.classList.remove('hidden'); + state.startupPending = false; + setBootstrapPending(false); + body?.classList?.add?.('dependency-onboarding-active'); + if (typeof showWelcome === 'boolean') { + state.showWelcome = showWelcome; + } else if (wasHidden) { + state.showWelcome = true; + } + if (state.diagnostics && Array.isArray(state.actions) && state.actions.length > 0) { + render(); + } + applyOnboardingLockUI(); + return true; + }; + + const setLoading = (loading) => { + state.loading = !!loading; + if (openBtn) openBtn.disabled = state.loading; + if (state.loading) { + summaryEl.textContent = ''; + } + }; + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))); + + const loadAndRender = async ({ open = false, forceAutoShow = false, bootstrap = false, explicitOpen = false } = {}) => { + if (state.loading) return false; + setLoading(true); + try { + const [persisted, diagRes, actionsRes] = await Promise.all([ + loadPersistedSetupState(), + fetch('/api/diagnostics'), + fetch('/api/setup-actions') + ]); + const diagData = await diagRes.json().catch(() => ({})); + const actionsData = await actionsRes.json().catch(() => ({})); + + if (!diagRes.ok || diagData?.ok === false) { + throw new Error(String(diagData?.error || `Diagnostics HTTP ${diagRes.status}`)); + } + if (!actionsRes.ok || actionsData?.ok === false) { + throw new Error(String(actionsData?.error || `Setup actions HTTP ${actionsRes.status}`)); + } + + state.diagnostics = diagData; + hydrateGitIdentityDraft(diagData); + const allActions = Array.isArray(actionsData?.actions) ? actionsData.actions : []; + const toolsMap = toToolMap(diagData); + state.actions = allActions.filter((action) => String(action?.id || '').trim() !== 'gh-login'); + const allowedActionIds = new Set( + state.actions + .map((action) => String(action?.id || '').trim()) + .filter(Boolean) + ); + const persistedSkippedIds = new Set( + (Array.isArray(persisted?.skippedActionIds) ? persisted.skippedActionIds : []) + .filter((id) => allowedActionIds.has(String(id || '').trim())) + ); + state.skippedActionIds = new Set( + Array.from(persistedSkippedIds) + ); + if (state.actions.length > 0) { + const savedStep = Math.max(0, Number(persisted?.currentStep) || 0); + setCurrentStep(savedStep, { persist: false }); + } + const view = render(); + applyOnboardingLockUI(); + + const hasCompletedOnboarding = readCompleted(); + const coreReady = !!view.req?.coreReady; + if (hasCompletedOnboarding) { + state.startupPending = false; + closeModal({ force: true }); + } + const shouldAutoShow = isWindowsHost && !hasCompletedOnboarding && (forceAutoShow || !readDismissed()); + const shouldKeepVisible = !hasCompletedOnboarding && open && !modal.classList.contains('hidden'); + if (explicitOpen || shouldKeepVisible || shouldAutoShow) { + openModal({ allowDuringStartup: bootstrap || explicitOpen }); + } else { + state.startupPending = false; + setBootstrapPending(false); + } + return true; + } catch (err) { + summaryEl.textContent = `Dependency check failed: ${String(err?.message || err)}`; + listEl.innerHTML = '
Unable to load setup actions right now.
'; + const shouldOpenOnError = explicitOpen || (open && !modal.classList.contains('hidden')); + if (shouldOpenOnError) openModal({ allowDuringStartup: bootstrap || explicitOpen }); + else if (!bootstrap) setBootstrapPending(false); + return false; + } finally { + setLoading(false); + } + }; + + const stopRunPolling = (actionId) => { + const id = String(actionId || '').trim(); + if (!id) return; + const poller = state.actionRunPollers.get(id); + if (poller?.timer) clearTimeout(poller.timer); + state.actionRunPollers.delete(id); + }; + + const updateActionRunState = (actionId, patch = {}, { rerender = true } = {}) => { + const id = String(actionId || '').trim(); + if (!id) return null; + const prev = state.actionRuns.get(id) || { actionId: id }; + const next = { + ...prev, + ...patch, + actionId: id + }; + state.actionRuns.set(id, next); + if (rerender) render(); + return next; + }; + + const fetchSetupActionRunStatus = async ({ runId = '', actionId = '' } = {}) => { + const params = new URLSearchParams(); + if (runId) params.set('runId', String(runId)); + if (actionId) params.set('actionId', String(actionId)); + const res = await fetch(`/api/setup-actions/run-status?${params.toString()}`); + const data = await res.json().catch(() => ({})); + if (!res.ok || data?.ok === false || !data?.run) { + throw new Error(String(data?.error || `HTTP ${res.status}`)); + } + return data.run; + }; + + const getVerifyPolicyForAction = (actionId) => { + const id = String(actionId || '').trim(); + if (id === 'gh-login') { + return { attempts: 14, delayMs: 900 }; + } + if (id === 'install-git' || id === 'install-node' || id === 'install-gh') { + return { attempts: 10, delayMs: 650 }; + } + return { attempts: 8, delayMs: 650 }; + }; + + const verifyActionInstalled = async (actionId, runId, options = {}) => { + const id = String(actionId || '').trim(); + if (!id) return false; + const policy = { + ...getVerifyPolicyForAction(id), + ...(options && typeof options === 'object' ? options : {}) + }; + const attempts = Math.max(1, Number(policy.attempts) || 1); + const delayMs = Math.max(250, Number(policy.delayMs) || 650); + + for (let attempt = 1; attempt <= attempts; attempt += 1) { + const runState = state.actionRuns.get(id); + if (!runState || String(runState?.runId || '') !== String(runId || '')) return false; + + updateActionRunState(id, { + status: 'verifying', + verifyAttempt: attempt, + verifyMax: attempts, + updatedAt: new Date().toISOString() + }); + + await loadAndRender({ open: true, forceAutoShow: true }); + const toolsMap = toToolMap(state.diagnostics); + if (isActionComplete(id, toolsMap)) { + updateActionRunState(id, { + status: 'verified', + verifyAttempt: attempts, + verifyMax: attempts, + updatedAt: new Date().toISOString() + }); + this.showToast('Dependency detected automatically.', 'success'); + return true; + } + await sleep(delayMs); + } + + updateActionRunState(id, { + status: 'needs-attention', + updatedAt: new Date().toISOString() + }); + this.showToast( + id === 'gh-login' + ? 'GitHub login is not detected yet. Complete sign-in in browser and try again.' + : 'Install finished but dependency is still missing. Review output and run again if needed.', + 'warning' + ); + return false; + }; + + const verifyActionWithoutRun = async (actionId, options = {}) => { + const id = String(actionId || '').trim(); + if (!id) return false; + const policy = { + ...getVerifyPolicyForAction(id), + ...(options && typeof options === 'object' ? options : {}) + }; + const attempts = Math.max(1, Number(policy.attempts) || 1); + const delayMs = Math.max(250, Number(policy.delayMs) || 650); + + for (let attempt = 1; attempt <= attempts; attempt += 1) { + updateActionRunState(id, { + status: 'verifying', + verifyAttempt: attempt, + verifyMax: attempts, + updatedAt: new Date().toISOString() + }); + + await loadAndRender({ open: true, forceAutoShow: true }); + const toolsMap = toToolMap(state.diagnostics); + if (isActionComplete(id, toolsMap)) { + updateActionRunState(id, { + status: 'verified', + verifyAttempt: attempts, + verifyMax: attempts, + updatedAt: new Date().toISOString() + }); + return true; + } + if (attempt < attempts) { + await sleep(delayMs); + } + } + + updateActionRunState(id, { + status: 'needs-attention', + updatedAt: new Date().toISOString() + }); + return false; + }; + + const pollRunUntilDone = async (actionId, runId) => { + const id = String(actionId || '').trim(); + const rid = String(runId || '').trim(); + if (!id || !rid) return; + + stopRunPolling(id); + const pollLoop = async () => { + try { + const run = await fetchSetupActionRunStatus({ runId: rid, actionId: id }); + updateActionRunState(id, run); + + if (String(run?.status || '').toLowerCase() === 'running') { + const timer = setTimeout(pollLoop, 850); + state.actionRunPollers.set(id, { runId: rid, timer }); + return; + } + + stopRunPolling(id); + await loadAndRender({ open: true, forceAutoShow: true }); + + if (String(run?.status || '').toLowerCase() === 'success') { + await verifyActionInstalled(id, rid); + return; + } + + if (String(run?.status || '').toLowerCase() === 'failed') { + if (id === 'gh-login') { + const ghRunLines = collectRunOutputLines(run, { limit: 160 }); + const ghRunInfo = extractGithubLoginInfo(ghRunLines); + const hasDeviceSignal = ghRunInfo.sawDeviceHint || !!ghRunInfo.code || /login\/device/i.test(ghRunLines.join('\n')); + const verifyOptions = hasDeviceSignal + ? { attempts: 14, delayMs: 900 } + : { attempts: 5, delayMs: 700 }; + const detected = await verifyActionWithoutRun(id, verifyOptions); + if (detected) { + this.showToast('GitHub login detected automatically.', 'success'); + return; + } + + const runError = String(run?.error || '').trim(); + const exitCode = Number(run?.exitCode); + const interrupted = exitCode === 1 || /code\s*1/i.test(runError) || /cancel|not completed/i.test(runError); + const missingDeviceCode = !hasDeviceSignal; + updateActionRunState(id, { + status: 'needs-attention', + error: interrupted + ? 'Login was not completed in browser.' + : (missingDeviceCode + ? 'GitHub CLI did not return a one-time code.' + : (runError || 'Login was not completed.')), + updatedAt: new Date().toISOString() + }); + this.showToast( + missingDeviceCode + ? 'GitHub CLI did not return a one-time code. Click Start login again.' + : (interrupted + ? 'GitHub login is still not detected. If browser sign-in just finished, click Start login again.' + : `GitHub login failed: ${runError || 'Unknown error'}`), + (interrupted || missingDeviceCode) ? 'warning' : 'error' + ); + return; + } + this.showToast(`Install failed: ${String(run?.error || 'Unknown error')}`, 'error'); + } + } catch (err) { + stopRunPolling(id); + updateActionRunState(id, { + status: 'failed', + error: String(err?.message || err || 'Failed to fetch setup status'), + updatedAt: new Date().toISOString() + }); + this.showToast(`Install monitoring failed: ${String(err?.message || err)}`, 'error'); + } + }; + + const timer = setTimeout(pollLoop, 250); + state.actionRunPollers.set(id, { runId: rid, timer }); + }; + + const runBootstrapLoad = async () => { + state.startupPending = true; + setBootstrapPending(true); + const delaysMs = [0, 240, 420, 700, 1050, 1450, 1900]; + for (let attempt = 0; attempt < delaysMs.length; attempt += 1) { + if (attempt > 0) { + await sleep(delaysMs[attempt]); + } + const ok = await loadAndRender({ open: false, forceAutoShow: false, bootstrap: true }); + if (ok) return; + } + state.startupPending = false; + setBootstrapPending(false); + }; + + const runSetupAction = async (actionId, btnEl) => { + const id = String(actionId || '').trim(); + if (!id) return; + const button = btnEl || null; + if (button) button.disabled = true; + try { + const existingRunStatus = String(state.actionRuns.get(id)?.status || '').trim().toLowerCase(); + if (existingRunStatus === 'running' || existingRunStatus === 'verifying' || existingRunStatus === 'success' || existingRunStatus === 'completed') { + this.showToast( + id === 'gh-login' + ? 'Login is still in progress. Please wait while we finish checking.' + : 'Install is still in progress. Please wait while we finish checking.', + 'info' + ); + return; + } + + const existingStep = getResolvedSteps().find((step) => String(step?.id || '').trim() === id); + const toolsMap = toToolMap(state.diagnostics); + if (id === 'gh-login' && !toToolMap(state.diagnostics).get('gh')) { + updateActionRunState(id, { + status: 'needs-attention', + error: 'Install GitHub CLI before starting login.', + updatedAt: new Date().toISOString() + }); + this.showToast('Install GitHub CLI first. Login is optional and only available after installation.', 'warning'); + await loadAndRender({ open: true, forceAutoShow: true }); + return; + } + if (id === 'install-codex' && !(toolsMap.get('node') && toolsMap.get('npm'))) { + updateActionRunState(id, { + status: 'needs-attention', + error: 'Install Node.js LTS first. Codex requires npm.', + updatedAt: new Date().toISOString() + }); + this.showToast('Install Node.js LTS first. Codex depends on npm.', 'warning'); + await loadAndRender({ open: true, forceAutoShow: true }); + return; + } + if (existingStep?.done) { + updateActionRunState(id, { + status: 'verified', + updatedAt: new Date().toISOString() + }); + this.showToast('Dependency already detected.', 'success'); + await loadAndRender({ open: true, forceAutoShow: true }); + return; + } + + updateActionRunState(id, { + runId: null, + status: 'running', + error: null, + output: [], + verifyAttempt: 0, + verifyMax: 0, + updatedAt: new Date().toISOString() + }); + const res = await fetch('/api/setup-actions/run', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ actionId: id }) + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || data?.ok === false) { + throw new Error(String(data?.error || `HTTP ${res.status}`)); } - if (!state.base) await refreshBase(); - await refreshInstallWizard().catch(() => {}); - render(state.base, state.firstRun, state.wizard); - - const appliedCount = Number(data?.appliedCount || 0); - const failedCount = Number(data?.failedCount || 0); - const skippedManualCount = Number(data?.skippedManualCount || 0); - if (failedCount > 0) { - this.showToast?.(`Auto-fix applied ${appliedCount}, failed ${failedCount}`, 'warning'); + const run = (data?.run && typeof data.run === 'object') ? data.run : null; + if (run) { + updateActionRunState(id, { + ...run, + verifyAttempt: 0, + verifyMax: 0 + }); + if (run?.runId) { + await pollRunUntilDone(id, run.runId); + } else { + await loadAndRender({ open: true, forceAutoShow: true }); + } } else { - const tail = skippedManualCount > 0 ? `, ${skippedManualCount} manual step(s) left` : ''; - this.showToast?.(`Auto-fix applied ${appliedCount}${tail}`, 'success'); + await loadAndRender({ open: true, forceAutoShow: true }); } - if (statusEl) statusEl.textContent = 'Safe auto-fix completed'; - } catch (error) { - this.showToast?.(`Safe auto-fix failed: ${String(error?.message || error)}`, 'error'); - if (statusEl) statusEl.textContent = ''; + const defaultMessage = data?.alreadyRunning + ? (id === 'gh-login' + ? 'GitHub login is already running. Complete it in your browser.' + : 'Install is already running. Watching for completion...') + : (id === 'gh-login' + ? 'GitHub login started. Complete sign-in in your browser.' + : 'Install started. We will check this step automatically.'); + this.showToast(String(data?.message || defaultMessage), 'info'); + } catch (err) { + updateActionRunState(id, { + status: 'failed', + error: String(err?.message || err), + updatedAt: new Date().toISOString() + }); + this.showToast(`Failed to start action: ${String(err?.message || err)}`, 'error'); } finally { - btnRepairSafe.disabled = false; + if (button) button.disabled = false; } - }); - repairEl?.addEventListener('click', async (event) => { - const target = event.target.closest('[data-diagnostics-repair]'); - if (!target) return; - const action = String(target.getAttribute('data-diagnostics-repair') || '').trim(); - if (!action) return; - target.disabled = true; - if (statusEl) statusEl.textContent = `Running repair: ${action}โ€ฆ`; + }; + + const saveGitIdentity = async (btnEl) => { + const button = btnEl || null; + const id = 'configure-git-identity'; + const nameInput = listEl.querySelector('[data-setup-git-name]'); + const emailInput = listEl.querySelector('[data-setup-git-email]'); + const name = String(nameInput?.value || state.gitIdentity?.name || '').trim(); + const email = String(emailInput?.value || state.gitIdentity?.email || '').trim(); + + state.gitIdentity.name = name; + state.gitIdentity.email = email; + + if (!name || !email) { + this.showToast('Enter both Git name and email.', 'warning'); + return; + } + + if (button) button.disabled = true; try { - const res = await fetch('/api/diagnostics/first-run/repair', { + updateActionRunState(id, { + runId: 'manual-git-identity', + status: 'running', + error: null, + output: [], + verifyAttempt: 0, + verifyMax: 0, + updatedAt: new Date().toISOString() + }); + + const res = await fetch('/api/setup-actions/configure-git-identity', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action }) + body: JSON.stringify({ name, email }) }); const data = await res.json().catch(() => ({})); if (!res.ok || data?.ok === false) { - throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`)); + throw new Error(String(data?.error || `HTTP ${res.status}`)); } - const repair = data?.repair || {}; - if (repair.manual) { - this.showToast?.(String(repair?.message || 'Manual action required'), 'warning'); + + state.gitIdentity.name = String(data?.name || name).trim(); + state.gitIdentity.email = String(data?.email || email).trim(); + + const detected = await verifyActionWithoutRun(id, { attempts: 6, delayMs: 350 }); + if (detected) { + this.showToast('Git identity saved and detected automatically.', 'success'); } else { - this.showToast?.(String(repair?.message || 'Repair completed'), 'success'); + this.showToast('Git identity saved, but detection is delayed. Try saving again in a few seconds.', 'warning'); } - if (data?.diagnostics) { - state.firstRun = data.diagnostics; - renderRepairActions(state.firstRun); - } else { - await refreshFirstRun(); - } - if (!state.base) await refreshBase(); - await refreshInstallWizard().catch(() => {}); - render(state.base, state.firstRun, state.wizard); - if (statusEl) statusEl.textContent = `Repair completed: ${action}`; - } catch (error) { - this.showToast?.(`Repair failed: ${String(error?.message || error)}`, 'error'); - if (statusEl) statusEl.textContent = ''; + } catch (err) { + updateActionRunState(id, { + status: 'failed', + error: String(err?.message || err), + updatedAt: new Date().toISOString() + }); + this.showToast(`Failed to save Git identity: ${String(err?.message || err)}`, 'error'); } finally { - target.disabled = false; + if (button) button.disabled = false; + await loadAndRender({ open: true, forceAutoShow: true }); + } + }; + + listEl.addEventListener('click', async (event) => { + const runBtn = event.target.closest('[data-setup-run]'); + if (runBtn) { + await runSetupAction(runBtn.getAttribute('data-setup-run'), runBtn); + return; + } + + const saveGitBtn = event.target.closest('[data-setup-git-save]'); + if (saveGitBtn) { + await saveGitIdentity(saveGitBtn); + return; + } + + const prevBtn = event.target.closest('[data-setup-prev]'); + if (prevBtn) { + setCurrentStep(state.currentStep - 1); + render(); + return; + } + + const nextBtn = event.target.closest('[data-setup-next]'); + if (nextBtn) { + const total = Array.isArray(state.actions) ? state.actions.length : 0; + const steps = getResolvedSteps(); + syncSkippedSteps(steps); + const currentStep = steps[state.currentStep]; + if (!currentStep?.done) { + if (!currentStep?.optional) { + this.showToast('Install this dependency before continuing.', 'warning'); + return; + } + setStepSkipped(currentStep?.id, true); + this.showToast('Skipping optional setup for now. You can configure it later.', 'warning'); + } else { + setStepSkipped(currentStep?.id, false); + } + if (state.currentStep >= (total - 1)) { + writeCompleted(true); + writeDismissed(false); + closeModal({ force: true }); + this.showToast('Dependency onboarding complete.', 'success'); + return; + } + setCurrentStep(state.currentStep + 1); + render(); + return; + } + const beginBtn = event.target.closest('[data-setup-begin]'); + if (beginBtn) { + state.showWelcome = false; + render(); + return; + } + + const jumpBtn = event.target.closest('[data-setup-jump]'); + if (jumpBtn) { + const idx = Number.parseInt(String(jumpBtn.getAttribute('data-setup-jump') || ''), 10); + if (Number.isFinite(idx)) { + setCurrentStep(idx); + render(); + } + return; + } + + const copyBtn = event.target.closest('[data-setup-copy-id]'); + if (copyBtn) { + const actionId = String(copyBtn.getAttribute('data-setup-copy-id') || '').trim(); + const action = (Array.isArray(state.actions) ? state.actions : []).find((item) => String(item?.id || '').trim() === actionId); + const command = String(action?.command || '').trim(); + if (!command) return; + try { + await navigator.clipboard.writeText(command); + this.showToast('Command copied to clipboard.', 'success'); + } catch (err) { + this.showToast(`Copy failed: ${String(err?.message || err)}`, 'error'); + } + return; + } + + const copyGhCodeBtn = event.target.closest('[data-setup-copy-gh-code]'); + if (copyGhCodeBtn) { + event.preventDefault(); + event.stopPropagation(); + const code = String(copyGhCodeBtn.getAttribute('data-setup-copy-gh-code') || '').trim(); + if (!code) return; + try { + await navigator.clipboard.writeText(code); + this.showToast('GitHub one-time code copied.', 'success'); + } catch (err) { + this.showToast(`Copy failed: ${String(err?.message || err)}`, 'error'); + } + return; + } + + const openGhLoginBtn = event.target.closest('[data-setup-open-gh-login]'); + if (openGhLoginBtn) { + event.preventDefault(); + event.stopPropagation(); + const link = String(openGhLoginBtn.getAttribute('data-setup-open-gh-login') || '').trim(); + if (!link) return; + try { + const res = await fetch('/api/setup-actions/open-url', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: link }) + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || data?.ok === false) { + throw new Error(String(data?.error || `HTTP ${res.status}`)); + } + this.showToast('Opened GitHub login in your browser.', 'info'); + } catch (err) { + this.showToast(`Could not open login link: ${String(err?.message || err)}`, 'error'); + } + return; + } + + const toggleGitHelpBtn = event.target.closest('[data-setup-toggle-git-help]'); + if (toggleGitHelpBtn) { + event.preventDefault(); + event.stopPropagation(); + state.gitIdentityHelpVisible = !state.gitIdentityHelpVisible; + render(); + return; } }); - } + + if (openBtn) { + openBtn.addEventListener('click', () => { + writeDismissed(false); + setCurrentStep(0); + loadAndRender({ open: true, forceAutoShow: true, explicitOpen: true }); + }); + } + if (closeBtn) { + closeBtn.addEventListener('click', () => closeModal()); + } + + modal.addEventListener('click', (event) => { + if (event.target === modal) closeModal(); + }); + + document.addEventListener('keydown', (event) => { + if (event.key !== 'Escape') return; + if (modal.classList.contains('hidden')) return; + closeModal(); + }); + + runBootstrapLoad(); + } notifyWorkflow({ type = 'info', message = '', sessionId = null, metadata = null } = {}) { const msg = String(message || '').trim(); @@ -9353,46 +10761,59 @@ class ClaudeOrchestrator { } } } - - showToast(message, type = 'info') { + + showToast(message, type = 'info', options = {}) { + const rawMessage = String(message || '').trim(); + if (!rawMessage) return; + + const normalizedType = (['info', 'success', 'warning', 'error'].includes(type)) ? type : 'info'; + const durationMsRaw = Number(options?.durationMs); + const durationMs = Number.isFinite(durationMsRaw) ? Math.max(1200, durationMsRaw) : 5000; + + let stack = document.getElementById('toast-stack'); + if (!stack) { + stack = document.createElement('div'); + stack.id = 'toast-stack'; + stack.className = 'toast-stack'; + document.body.appendChild(stack); + } + + const iconByType = { + info: '', + success: '', + warning: '', + error: '' + }; + const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; + toast.className = `toast toast-${normalizedType}`; + toast.setAttribute('role', 'status'); toast.innerHTML = `
- ${type === 'success' ? 'โœ…' : type === 'warning' ? 'โš ๏ธ' : type === 'error' ? 'โŒ' : 'โ„น๏ธ'} - ${message} + ${iconByType[normalizedType] || iconByType.info} + ${this.escapeHtml(rawMessage)}
+ `; - - // Add styles for different toast types - const styles = { - info: 'var(--accent-primary)', - success: 'var(--accent-success)', - warning: 'var(--accent-warning)', - error: 'var(--accent-danger)' - }; - - toast.style.cssText = ` - position: fixed; - top: calc(var(--header-height) + 20px); - right: 20px; - background: ${styles[type]}; - color: white; - padding: var(--space-sm) var(--space-md); - border-radius: var(--radius-md); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - animation: slideInRight 0.3s ease-out, fadeOutRight 0.3s ease-in 4.7s forwards; - `; - - document.body.appendChild(toast); - - // Remove after 5 seconds - setTimeout(() => { - if (toast.parentNode) { + + const removeToast = () => { + if (!toast.parentNode) return; + if (toast.classList.contains('is-leaving')) return; + toast.classList.add('is-leaving'); + setTimeout(() => { toast.remove(); - } - }, 5000); + if (stack && !stack.children.length) { + stack.remove(); + } + }, 240); + }; + + const closeBtn = toast.querySelector('.toast-close'); + closeBtn?.addEventListener('click', removeToast); + + stack.appendChild(toast); + requestAnimationFrame(() => toast.classList.add('is-visible')); + setTimeout(removeToast, durationMs); } async launchDiffViewer(githubUrl) { @@ -9400,9 +10821,9 @@ class ClaudeOrchestrator { const prMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/); const commitMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/commit\/([a-f0-9]{40})/); const compareMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/compare\/([^?#]+)/); - + let diffViewerPath = ''; - + if (prMatch) { const [, owner, repo, pr] = prMatch; diffViewerPath = `/pr/${owner}/${repo}/${pr}`; @@ -9430,7 +10851,7 @@ class ClaudeOrchestrator { this.showToast('Unable to parse GitHub URL', 'error'); return; } - + // Open a placeholder tab immediately (avoids popup blockers), then redirect once ready. const popup = window.open('', '_blank'); if (!popup) { @@ -12743,19 +14164,7 @@ class ClaudeOrchestrator { }); bodyEl.querySelectorAll?.('[data-open-diagnostics="true"]')?.forEach?.((btn) => { btn.addEventListener('click', () => { - try { - document.getElementById('settings-panel')?.classList?.remove?.('hidden'); - setTimeout(() => { - try { - document.getElementById('diagnostics-output')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); - } catch {} - try { - document.getElementById('diagnostics-refresh')?.click?.(); - } catch {} - }, 50); - } catch { - // ignore - } + this.openDiagnosticsPanel({ refresh: true }); }); }); bodyEl.querySelectorAll('[data-pr-refresh]').forEach((btn0) => { @@ -12899,7 +14308,7 @@ class ClaudeOrchestrator { // Check URL params first const urlParams = new URLSearchParams(window.location.search); const tokenFromUrl = urlParams.get('token'); - + if (tokenFromUrl) { // Save to localStorage for future use localStorage.setItem('claude-orchestrator-token', tokenFromUrl); @@ -12907,7 +14316,7 @@ class ClaudeOrchestrator { window.history.replaceState({}, document.title, window.location.pathname); return tokenFromUrl; } - + // Check localStorage return localStorage.getItem('claude-orchestrator-token'); } @@ -13733,7 +15142,7 @@ class ClaudeOrchestrator { this.showToast?.(`Pruned ${prunedCount} old recoverable session(s)`, 'success'); return true; } - + installAuthFetchShim() { if (window.__claudeOrchestratorFetchAuthInstalled) return; if (typeof window.fetch !== 'function') return; @@ -13777,7 +15186,7 @@ class ClaudeOrchestrator { window.__claudeOrchestratorFetchAuthInstalled = true; } - + // Terminal Focus Feature - Now shows only that worktree focusTerminal(sessionId) { // Extract worktree ID from session ID @@ -13815,14 +15224,14 @@ class ClaudeOrchestrator { console.error(`Terminal instance not found for ${sessionId}`); return; } - + // Store original parent for unfocus const terminalElement = terminalWrapper.querySelector('.terminal'); if (!terminalElement) { console.error(`Terminal element not found in wrapper for ${sessionId}`); return; } - + this.focusedTerminalInfo = { sessionId: sessionId, originalParent: terminalElement.parentElement, @@ -13834,41 +15243,41 @@ class ClaudeOrchestrator { rows: xtermInstance.rows || 24 } }; - + // Add focusing animation to original terminal terminalWrapper.classList.add('focusing'); - + // Update overlay header const focusedTitle = document.getElementById('focused-title'); const focusedBranch = document.getElementById('focused-branch'); const focusedStatus = document.getElementById('focused-status'); - + const isAgentSession = /-(claude|codex)$/.test(String(sessionId || '')); const worktreeNumber = sessionId.split('-')[0].replace('work', ''); - + if (focusedTitle) focusedTitle.textContent = `${isAgentSession ? '๐Ÿค– Agent' : '๐Ÿ’ป Server'} ${worktreeNumber}`; if (focusedBranch) focusedBranch.textContent = this.formatBranchLabel(session.branch || '', { context: 'terminal' }).text || ''; if (focusedStatus) focusedStatus.className = `status-indicator ${session.status || 'idle'}`; - + // Move the actual terminal element to focused container const focusedTerminalBody = document.getElementById('focused-terminal-body'); if (!focusedTerminalBody) { console.error('Focused terminal body container not found'); return; } - + focusedTerminalBody.innerHTML = ''; focusedTerminalBody.appendChild(terminalElement); - + // Hide original wrapper terminalWrapper.style.visibility = 'hidden'; - + // Activate focus overlay with animation const focusOverlay = document.getElementById('focus-overlay'); if (focusOverlay) { focusOverlay.classList.add('active'); } - + // Bind ESC key for unfocus this.handleEscKey = (e) => { if (e.key === 'Escape') { @@ -13876,41 +15285,41 @@ class ClaudeOrchestrator { } }; document.addEventListener('keydown', this.handleEscKey); - + // Resize terminal to fit the focused container after animation setTimeout(() => { try { // Store original font size this.focusedTerminalInfo.originalFontSize = xtermInstance.options.fontSize || 12; - + // Increase font size for better readability in focused mode const originalSize = this.focusedTerminalInfo.originalFontSize; const newFontSize = Math.round(originalSize * 1.8); // 1.8x larger (reduced from 3x by ~60%) xtermInstance.options.fontSize = newFontSize; - + const rect = focusedTerminalBody.getBoundingClientRect(); // Calculate new dimensions based on container size with larger font const charWidth = newFontSize * 0.6; // Approximate character width const lineHeight = newFontSize * 1.4; // Approximate line height - + const cols = Math.floor((rect.width - 30) / charWidth); const rows = Math.floor((rect.height - 30) / lineHeight); - + // Apply reasonable limits const finalCols = Math.min(200, Math.max(80, cols)); const finalRows = Math.min(80, Math.max(24, rows)); - + console.log(`Resizing focused terminal from ${xtermInstance.cols}x${xtermInstance.rows} to ${finalCols}x${finalRows} with font size ${newFontSize}px`); - + // Resize xterm xtermInstance.resize(finalCols, finalRows); - + // Use fit addon if available const fitAddon = this.terminalManager?.fitAddons?.get(sessionId); if (fitAddon) { fitAddon.fit(); } - + // Send resize command to backend if (this.socket) { this.socket.emit('resize', { @@ -13919,46 +15328,46 @@ class ClaudeOrchestrator { rows: finalRows }); } - + // Focus the terminal for input xtermInstance.focus(); } catch (resizeError) { console.error('Error resizing focused terminal:', resizeError); } }, 200); - + // Remove focusing animation after transition setTimeout(() => { terminalWrapper.classList.remove('focusing'); }, 300); - + } catch (error) { console.error('Error focusing terminal:', error); } } - + unfocusTerminal() { try { if (!this.focusedTerminalInfo) return; - + const { sessionId, originalParent, originalNextSibling, terminalElement, terminalWrapper, originalDimensions } = this.focusedTerminalInfo; - + // Move terminal element back to original location if (originalNextSibling) { originalParent.insertBefore(terminalElement, originalNextSibling); } else { originalParent.appendChild(terminalElement); } - + // Show original wrapper terminalWrapper.style.visibility = 'visible'; - + // Deactivate focus overlay const focusOverlay = document.getElementById('focus-overlay'); if (focusOverlay) { focusOverlay.classList.remove('active'); } - + // Restore original terminal size and font const xtermInstance = this.terminalManager?.terminals?.get(sessionId); if (xtermInstance) { @@ -13966,21 +15375,21 @@ class ClaudeOrchestrator { const originalFontSize = this.focusedTerminalInfo.originalFontSize || 12; console.log(`Restoring font size from ${xtermInstance.options.fontSize}px to ${originalFontSize}px`); xtermInstance.options.fontSize = originalFontSize; - + // Force a refresh of the terminal to apply font change xtermInstance.refresh(0, xtermInstance.rows - 1); - + if (originalDimensions) { setTimeout(() => { console.log(`Restoring terminal dimensions to ${originalDimensions.cols}x${originalDimensions.rows}`); xtermInstance.resize(originalDimensions.cols, originalDimensions.rows); - + // Use fit addon if available const fitAddon = this.terminalManager?.fitAddons?.get(sessionId); if (fitAddon) { setTimeout(() => fitAddon.fit(), 50); } - + // Send resize command to backend if (this.socket) { this.socket.emit('resize', { @@ -13992,10 +15401,10 @@ class ClaudeOrchestrator { }, 100); } } - + // Clean up this.focusedTerminalInfo = null; - + // Remove ESC key listener if (this.handleEscKey) { document.removeEventListener('keydown', this.handleEscKey); @@ -14005,14 +15414,14 @@ class ClaudeOrchestrator { console.error('Error unfocusing terminal:', error); } } - + calculateTerminalDimensions(container) { if (!container) return null; - + const rect = container.getBoundingClientRect(); const cols = Math.floor(rect.width / 9); // Approximate character width const rows = Math.floor(rect.height / 20); // Approximate line height - + return { cols: Math.max(80, cols), rows: Math.max(24, rows) }; } @@ -14149,7 +15558,7 @@ class ClaudeOrchestrator { if (startupUI) { startupUI.style.display = 'none'; } - + } catch (error) { console.error('Error auto-starting Claude:', error); this.showError('Failed to start Claude with settings'); @@ -14178,7 +15587,7 @@ class ClaudeOrchestrator { } } } - + hideClaudeStartupModal() { const modal = document.getElementById('claude-startup-modal'); if (modal) { @@ -14186,7 +15595,7 @@ class ClaudeOrchestrator { this.pendingClaudeSession = null; } } - + async startClaudeWithOptions(sessionId, mode, skipPermissions) { if (!this.socket || !this.socket.connected) { this.showError('Not connected to server'); @@ -14316,7 +15725,7 @@ class ClaudeOrchestrator { if (startupUI) startupUI.style.display = 'none'; this.dismissedStartupUI.set(sid, true); } - + quickStartClaude(sessionId, mode) { // Check if YOLO mode is enabled const yoloCheckbox = document.getElementById(`yolo-${sessionId}`); @@ -14817,7 +16226,7 @@ class ClaudeOrchestrator { document.addEventListener('keydown', handleEsc); }); } - + updateYoloState(sessionId, checked) { // Update button styles to show YOLO is active const buttons = [ @@ -14825,7 +16234,7 @@ class ClaudeOrchestrator { document.getElementById(`btn-continue-${sessionId}`), document.getElementById(`btn-resume-${sessionId}`) ]; - + buttons.forEach(btn => { if (btn) { if (checked) { @@ -14836,25 +16245,25 @@ class ClaudeOrchestrator { } }); } - + async startClaudeFromTerminal(sessionId) { if (!this.socket || !this.socket.connected) { return; } - + try { // Get effective settings for this session const response = await fetch(`/api/user-settings/effective/${sessionId}`); let effectiveSettings = { claudeFlags: { skipPermissions: false } }; - + if (response.ok) { effectiveSettings = await response.json(); } - + // Get selected options from the inline UI, but use effective settings as fallback const mode = document.querySelector(`input[name="claude-mode-${sessionId}"]:checked`)?.value || 'fresh'; const skipPermissions = document.getElementById(`skip-permissions-${sessionId}`)?.checked ?? effectiveSettings.claudeFlags.skipPermissions; - + // Send command to server this.socket.emit('start-claude', { sessionId: sessionId, @@ -14863,19 +16272,19 @@ class ClaudeOrchestrator { skipPermissions: skipPermissions } }); - + // Hide the startup UI const startupUI = document.getElementById(this.getSessionDomId('startup-ui', sessionId)); if (startupUI) { startupUI.style.display = 'none'; } - + // Enable the start button for future use const startBtn = document.getElementById(`claude-start-btn-${sessionId}`); if (startBtn) { startBtn.disabled = false; } - + } catch (error) { console.error('Error starting Claude from terminal:', error); } @@ -14883,10 +16292,10 @@ class ClaudeOrchestrator { restartClaudeSession(sessionId) { console.log(`Restarting Claude session: ${sessionId}`); - + if (this.socket && this.socket.connected) { this.socket.emit('restart-session', { sessionId }); - + // Update UI to show restarting this.updateSessionStatus(sessionId, 'restarting'); } else { @@ -14922,16 +16331,16 @@ class ClaudeOrchestrator { if (!this.userSettings) { console.warn('User settings not loaded, attempting to load...'); await this.loadUserSettings(); - + if (!this.userSettings) { console.error('Failed to load user settings'); return; } } - + const pathParts = path.split('.'); const newGlobal = JSON.parse(JSON.stringify(this.userSettings.global)); - + // Navigate to the correct nested property let current = newGlobal; for (let i = 0; i < pathParts.length - 1; i++) { @@ -15556,7 +16965,7 @@ class ClaudeOrchestrator { this.userSettings = updatedSettings; this.syncUserSettingsUI(); console.log('Reset to defaults successfully'); - + // Show user feedback this.showTemporaryMessage('Settings reset to defaults'); } else { @@ -15582,7 +16991,7 @@ class ClaudeOrchestrator { if (response.ok) { console.log('Saved as default template successfully'); - + // Show user feedback with commit reminder this.showTemporaryMessage('Settings saved as default template. Remember to commit and push the changes to user-settings.default.json!', 'success'); } else { @@ -15600,7 +17009,7 @@ class ClaudeOrchestrator { const messageEl = document.createElement('div'); messageEl.className = `temporary-message ${type}`; messageEl.textContent = message; - + // Style the message messageEl.style.cssText = ` position: fixed; @@ -15616,14 +17025,14 @@ class ClaudeOrchestrator { transform: translateX(100%); transition: transform 0.3s ease; `; - + document.body.appendChild(messageEl); - + // Animate in setTimeout(() => { messageEl.style.transform = 'translateX(0)'; }, 100); - + // Remove after delay setTimeout(() => { messageEl.style.transform = 'translateX(100%)'; @@ -15642,9 +17051,9 @@ class ClaudeOrchestrator { this.showTemporaryMessage('Invalid session ID for replay viewer', 'error'); return; } - + const worktreeNum = worktreeMatch[1]; - + // Get worktree configuration from server for accurate path let worktreeConfig = null; try { @@ -15655,19 +17064,19 @@ class ClaudeOrchestrator { } catch (error) { console.warn('Could not get worktree config, using defaults:', error); } - + // Use server-hosted replay viewer (avoids browser file:// restrictions) const replayViewerUrl = `${window.location.origin}/replay-viewer/work${worktreeNum}/`; - + console.log(`Opening replay viewer for ${sessionId} at ${replayViewerUrl}`); - + // Open in new tab (simpler approach) window.open(replayViewerUrl, '_blank'); - + // Show success message with URL for reference this.showTemporaryMessage(`Opening replay viewer for work${worktreeNum}`, 'success'); console.log(`Replay viewer URL: ${replayViewerUrl}`); - + } catch (error) { console.error('Error opening replay viewer:', error); this.showTemporaryMessage('Failed to open replay viewer', 'error'); @@ -15687,7 +17096,7 @@ class ClaudeOrchestrator { setTimeout(checkAndStart, 500); // Check again in 500ms } }; - + setTimeout(checkAndStart, 1000); // Initial delay for terminal setup } @@ -15696,7 +17105,7 @@ class ClaudeOrchestrator { const response = await fetch('/api/user-settings/check-updates'); if (response.ok) { const result = await response.json(); - + if (result && result.hasUpdates) { const notification = document.getElementById('settings-update-notification'); notification.classList.remove('hidden'); @@ -15802,17 +17211,17 @@ class ClaudeOrchestrator { try { this.showTemporaryMessage('Checking for updates...', 'info'); - + const response = await fetch('/api/git/check-updates'); if (response.ok) { const result = await response.json(); - + if (result.hasUpdates) { const notification = document.getElementById('git-update-notification'); const textElement = document.getElementById('git-notification-text'); textElement.textContent = `${result.commitsBehind} update${result.commitsBehind > 1 ? 's' : ''} available on ${result.currentBranch}`; notification.classList.remove('hidden'); - + this.showTemporaryMessage(`Found ${result.commitsBehind} update${result.commitsBehind > 1 ? 's' : ''} available`, 'success'); } else if (result.hasUpdates === false) { this.showTemporaryMessage('Repository is up to date', 'success'); @@ -15835,7 +17244,7 @@ class ClaudeOrchestrator { } this.showTemporaryMessage('Pulling latest changes...', 'info'); - + const response = await fetch('/api/git/pull', { method: 'POST', headers: { 'Content-Type': 'application/json' } @@ -15843,14 +17252,14 @@ class ClaudeOrchestrator { if (response.ok) { const result = await response.json(); - + if (result.success) { // Success message will be handled by socket event const notification = document.getElementById('git-update-notification'); notification.classList.add('hidden'); } else { this.showTemporaryMessage(result.error || 'Failed to pull changes', 'error'); - + // Show specific error details if available if (result.changes && result.changes.length > 0) { console.log('Uncommitted changes:', result.changes); @@ -19679,7 +21088,7 @@ class ClaudeOrchestrator { applyView(); return; } - + state.selectedCardId = card.id || null; applyView(); @@ -21029,7 +22438,7 @@ class ClaudeOrchestrator { col.style.setProperty('--tasks-card-rows', '1'); return; } - + const containerHeight = cardsContainer.clientHeight; if (!containerHeight || containerHeight < 40) { col.style.setProperty('--tasks-card-columns', '1'); @@ -21047,7 +22456,7 @@ class ClaudeOrchestrator { return; } delete col.dataset.tasksWrapExpandRetry; - + const styles = window.getComputedStyle(cardsContainer); const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0; const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0; @@ -21092,10 +22501,10 @@ class ClaudeOrchestrator { col.style.minWidth = `${Math.round(target)}px`; } }; - + apply(rowsFit); const fits = () => (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1); - + // If we still overflow vertically, reduce rows (creating more columns) until we fit. for (let attempt = 0; attempt < 24; attempt++) { // Force reflow and then check overflow. @@ -30324,7 +31733,7 @@ class ClaudeOrchestrator { // While worktree sessions are spinning up, we reserve the worktree so it isn't recommended again. this.cleanupExpiredWorktreeReservations(); if (this.isWorktreeReserved(repoPathNorm, worktreeId)) return true; - + // Extract repo name from path for session matching const repoName = (repoNameOverride || repoPathNorm.split('/').pop() || '').toLowerCase(); diff --git a/client/index.html b/client/index.html index 0605696e..e3c58b56 100644 --- a/client/index.html +++ b/client/index.html @@ -1,5 +1,6 @@ + @@ -7,16 +8,18 @@ - + - + - + +
- +
@@ -131,7 +134,7 @@

Claude Orchestrator

- + - + - +
-

Configure board-specific Done/For Test lists in Tasks โ†’ Board Settings โ†’ Conventions.

+

Configure board-specific Done/For Test lists in Tasks โ†’ Board + Settings โ†’ Conventions.

- +

Terminal Settings

- +
- +
@@ -939,10 +958,11 @@

Terminal Settings

Per-Terminal Overrides
-

Override global settings for specific terminals (requires terminal restart)

+

Override global settings for specific terminals (requires terminal + restart)

- +
Default Template Management

Manage the default settings template committed to the repository

@@ -962,10 +982,11 @@
Default Template Management
- +
Repository Updates
-

Web/dev mode: Git pull updates. Tauri desktop mode: app updater check/install.

+

Web/dev mode: Git pull updates. Tauri desktop mode: app updater + check/install.

+ + + - + - + @@ -1035,6 +1075,7 @@

Notifications

+
@@ -1044,10 +1085,12 @@

Notifications

- +
- +
@@ -1062,4 +1105,5 @@

Notifications

+ diff --git a/client/notifications.js b/client/notifications.js index 6fa218c3..7ec3ea9d 100644 --- a/client/notifications.js +++ b/client/notifications.js @@ -389,21 +389,21 @@ class NotificationManager { // Add notification styles const notificationStyles = document.createElement('style'); notificationStyles.textContent = ` - .empty-message { + .notifications-panel .empty-message { padding: var(--space-xl); text-align: center; color: var(--text-secondary); font-size: 0.875rem; } - .notification-header { + .notifications-panel .notification-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-xs); } - .notification-meta { + .notifications-panel .notification-meta { font-size: 0.75rem; color: var(--text-secondary); margin-top: var(--space-xs); diff --git a/client/styles.css b/client/styles.css index eab6a28d..62b1bf22 100644 --- a/client/styles.css +++ b/client/styles.css @@ -79,6 +79,15 @@ --shadow-float: 0 18px 40px rgba(0, 0, 0, 0.45); --shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.15); --focus-terminal-bg: color-mix(in srgb, var(--bg-primary) 72%, #000000 28%); + + /* Scrollbars */ + --scrollbar-size: 10px; + --scrollbar-radius: 999px; + --scrollbar-track: #1b2230; + --scrollbar-thumb: #3f5f8f; + --scrollbar-thumb-hover: #4f76ae; + --scrollbar-thumb-active: #5a84bf; + --scrollbar-thumb-border: rgba(13, 17, 23, 0.72); } /* Light Theme */ @@ -107,6 +116,30 @@ body.light-theme { --shadow-float: 0 18px 40px rgba(0, 0, 0, 0.22); --shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.12); --focus-terminal-bg: color-mix(in srgb, var(--bg-primary) 94%, #000000 6%); + + --scrollbar-track: #dbe1e8; + --scrollbar-thumb: #7f91aa; + --scrollbar-thumb-hover: #6f86a8; + --scrollbar-thumb-active: #5d789d; + --scrollbar-thumb-border: rgba(255, 255, 255, 0.76); +} + +@supports (color: color-mix(in srgb, #000000 50%, #ffffff 50%)) { + :root { + --scrollbar-track: color-mix(in srgb, var(--bg-secondary) 78%, #000000 22%); + --scrollbar-thumb: color-mix(in srgb, var(--accent-primary) 46%, var(--bg-tertiary) 54%); + --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-primary-hover) 60%, var(--bg-tertiary) 40%); + --scrollbar-thumb-active: color-mix(in srgb, var(--accent-primary) 72%, var(--bg-tertiary) 28%); + --scrollbar-thumb-border: color-mix(in srgb, var(--bg-primary) 76%, transparent 24%); + } + + body.light-theme { + --scrollbar-track: color-mix(in srgb, var(--bg-tertiary) 70%, #ffffff 30%); + --scrollbar-thumb: color-mix(in srgb, var(--accent-primary) 44%, #7f8a98 56%); + --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-primary) 58%, #6f7c8f 42%); + --scrollbar-thumb-active: color-mix(in srgb, var(--accent-primary) 68%, #617187 32%); + --scrollbar-thumb-border: color-mix(in srgb, #ffffff 68%, transparent 32%); + } } /* Skins */ @@ -178,6 +211,133 @@ body.light-theme.skin-high-contrast { margin: 0; padding: 0; box-sizing: border-box; + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} + +*::-webkit-scrollbar { + width: var(--scrollbar-size); + height: var(--scrollbar-size); +} + +*::-webkit-scrollbar-track { + background: var(--scrollbar-track); + border-radius: var(--scrollbar-radius); +} + +*::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: var(--scrollbar-radius); + border: 2px solid var(--scrollbar-thumb-border); + background-clip: padding-box; +} + +*::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} + +*::-webkit-scrollbar-thumb:active { + background: var(--scrollbar-thumb-active); +} + +*::-webkit-scrollbar-corner { + background: var(--scrollbar-track); +} + +:where( + .onboarding-overlay, + .onboarding-container, + .dependency-setup-body, + .dependency-setup-item-output, + .diagnostics-output, + .worktree-list, + .workspace-tabs-container, + .header-actions, + .terminal .xterm-viewport, + .focused-terminal-body .xterm-viewport +) { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} + +:where( + .onboarding-overlay, + .onboarding-container, + .dependency-setup-body, + .dependency-setup-item-output, + .diagnostics-output, + .worktree-list, + .workspace-tabs-container, + .header-actions, + .terminal .xterm-viewport, + .focused-terminal-body .xterm-viewport +)::-webkit-scrollbar { + width: var(--scrollbar-size); + height: var(--scrollbar-size); +} + +:where( + .onboarding-overlay, + .onboarding-container, + .dependency-setup-body, + .dependency-setup-item-output, + .diagnostics-output, + .worktree-list, + .workspace-tabs-container, + .header-actions, + .terminal .xterm-viewport, + .focused-terminal-body .xterm-viewport +)::-webkit-scrollbar-track { + background: var(--scrollbar-track); + border-radius: var(--scrollbar-radius); +} + +:where( + .onboarding-overlay, + .onboarding-container, + .dependency-setup-body, + .dependency-setup-item-output, + .diagnostics-output, + .worktree-list, + .workspace-tabs-container, + .header-actions, + .terminal .xterm-viewport, + .focused-terminal-body .xterm-viewport +)::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: var(--scrollbar-radius); + border: 2px solid var(--scrollbar-thumb-border); + background-clip: padding-box; +} + +:where( + .onboarding-overlay, + .onboarding-container, + .dependency-setup-body, + .dependency-setup-item-output, + .diagnostics-output, + .worktree-list, + .workspace-tabs-container, + .header-actions, + .terminal .xterm-viewport, + .focused-terminal-body .xterm-viewport +)::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} + +:where( + .onboarding-overlay, + .onboarding-container, + .dependency-setup-body, + .dependency-setup-item-output, + .diagnostics-output, + .worktree-list, + .workspace-tabs-container, + .header-actions, + .terminal .xterm-viewport, + .focused-terminal-body .xterm-viewport +)::-webkit-scrollbar-thumb:active { + background: var(--scrollbar-thumb-active); } body { @@ -2143,12 +2303,19 @@ header h1 { } .worktree-inspector-header.review-console-header::-webkit-scrollbar { - height: 6px; + height: 8px; +} + +.worktree-inspector-header.review-console-header::-webkit-scrollbar-track { + background: var(--scrollbar-track); + border-radius: var(--scrollbar-radius); } .worktree-inspector-header.review-console-header::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.16); - border-radius: 999px; + background: var(--scrollbar-thumb); + border-radius: var(--scrollbar-radius); + border: 2px solid var(--scrollbar-thumb-border); + background-clip: padding-box; } .review-console-route-bar { @@ -4354,19 +4521,23 @@ header h1 { line-height: 1.35; } -.settings-toolbar input { - flex: 1 1 auto; - min-width: 0; - padding: 0 10px; +body.dependency-onboarding-active { + overflow: hidden; + background: + radial-gradient(circle at 12% 18%, rgba(56, 139, 253, 0.16), transparent 48%), + radial-gradient(circle at 88% 8%, rgba(34, 197, 94, 0.14), transparent 44%), + var(--bg-primary); } -.settings-toolbar select { - flex: 0 0 auto; - min-width: 120px; - padding: 0 8px; +body.dependency-onboarding-booting { + overflow: hidden; + background: + radial-gradient(circle at 14% 22%, rgba(56, 139, 253, 0.14), transparent 46%), + radial-gradient(circle at 82% 10%, rgba(34, 197, 94, 0.12), transparent 42%), + var(--bg-primary); } -.settings-filter-hidden { +body.dependency-onboarding-booting > :not(#dependency-setup-modal):not(#toast-stack):not(.toast):not(.ready-toast):not(#notifications-panel):not(.cross-workspace-notifications-area) { display: none !important; } @@ -4416,57 +4587,45 @@ header h1 { border-radius: 10px; } -.setting-group { - margin-bottom: var(--space-md); +body.dependency-onboarding-active > :not(#dependency-setup-modal):not(#toast-stack):not(.toast):not(.ready-toast):not(#notifications-panel):not(.cross-workspace-notifications-area) { + display: none !important; } -.setting-group label { +body.dependency-onboarding-active #dependency-setup-modal { display: flex; align-items: center; - gap: var(--space-sm); - cursor: pointer; -} - -.setting-group select { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border-color); - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius-sm); - margin-left: var(--space-sm); + justify-content: center; + padding: 24px; + z-index: 4500; } -.setting-section { - margin: var(--space-lg) 0; - padding-top: var(--space-lg); - border-top: 1px solid var(--border-color); +.dependency-setup-content { + max-width: min(760px, 94vw); + width: min(760px, 94vw); + border: 1px solid rgba(56, 139, 253, 0.2); + box-shadow: 0 24px 54px rgba(0, 0, 0, 0.35); } -.setting-section h4 { - color: var(--text-primary); - margin-bottom: var(--space-md); - font-size: 1.1em; +.dependency-setup-content .modal-header { + margin-bottom: 10px; } -.setting-section h5 { - color: var(--text-secondary); - margin-bottom: var(--space-sm); - font-size: 0.9em; - font-weight: 600; +.dependency-setup-content .modal-header h3 { + font-size: 1.2rem; + letter-spacing: 0.01em; } -.setting-group label small { - display: block; - color: var(--text-tertiary); - font-size: 0.8em; - margin-top: var(--space-xs); - margin-left: var(--space-lg); +.dependency-setup-body { + max-height: 68vh; + overflow: auto; + padding-right: 4px; } -.setting-description { - color: var(--text-tertiary); - font-size: 0.85em; - margin-bottom: var(--space-md); +.dependency-setup-list { + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 12px; } .skin-gallery { @@ -4539,612 +4698,1216 @@ header h1 { .settings-glossary details { border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - padding: 8px 10px; + border-radius: var(--radius-lg); background: var(--bg-tertiary); - margin: 10px 0; -} - -.settings-glossary details[open] { - background: rgba(255, 255, 255, 0.02); + padding: 14px; } -.settings-glossary summary { - cursor: pointer; +.dependency-setup-item-header { display: flex; align-items: center; - gap: 8px; - list-style: none; -} - -.settings-glossary summary::-webkit-details-marker { - display: none; + justify-content: space-between; + gap: 10px; + margin-bottom: 6px; + flex-wrap: wrap; } -.settings-glossary details > .setting-description { - margin: 10px 0 0; - opacity: 0.95; +.dependency-setup-item-title { + font-weight: 650; + font-size: 1.03rem; + color: var(--text-primary); } -.identity-saved-list { +.dependency-setup-badges { display: flex; - flex-wrap: wrap; + align-items: center; gap: 6px; + flex-wrap: wrap; } -.identity-chip { +.dependency-setup-badge { display: inline-flex; align-items: center; - gap: 6px; - padding: 2px 8px; - border-radius: var(--radius-sm); + border-radius: 999px; border: 1px solid var(--border-color); - background: rgba(255, 255, 255, 0.03); - font-family: var(--font-mono); - font-size: 0.78rem; - max-width: 100%; + padding: 2px 8px; + font-size: 0.75rem; + color: var(--text-secondary); + background: var(--bg-secondary); } -.identity-chip button { - border: none; - background: transparent; - color: var(--text-tertiary); - cursor: pointer; - padding: 0; - line-height: 1; +.dependency-setup-badge.status-ok { + color: var(--accent-success); + border-color: rgba(34, 197, 94, 0.45); } -.identity-chip button:hover { - color: var(--text-secondary); +.dependency-setup-badge.status-missing { + color: var(--accent-warning); + border-color: rgba(245, 158, 11, 0.45); } -.per-terminal-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-sm); - margin-bottom: var(--space-xs); - background: var(--bg-tertiary); - border-radius: var(--radius-sm); +.dependency-setup-badge.status-pending { + color: var(--accent-primary); + border-color: rgba(56, 139, 253, 0.45); } -.per-terminal-item .terminal-name { - color: var(--text-primary); - font-weight: 500; - font-family: var(--font-mono); +.dependency-setup-badge.level-required { + color: var(--accent-danger); + border-color: rgba(239, 68, 68, 0.45); } -.per-terminal-item .terminal-override { - color: var(--text-secondary); - font-size: 0.8em; +.dependency-setup-badge.level-recommended { + color: var(--accent-primary); + border-color: rgba(56, 139, 253, 0.45); } -.per-terminal-items { - margin-top: var(--space-sm); +.dependency-setup-badge.level-optional { + color: var(--accent-warning); + border-color: rgba(245, 158, 11, 0.45); } -/* Notifications Panel */ -.notifications-panel { - position: fixed; - top: var(--header-height); - right: 0; - bottom: 0; - width: 380px; +.dependency-setup-item-desc { + color: var(--text-secondary); + margin-bottom: 10px; + line-height: 1.45; +} + +.dependency-setup-item-command { + margin: 0; + padding: 8px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); background: var(--bg-secondary); - border-left: 1px solid var(--border-color); - box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15); - transform: translateX(100%); - transition: transform 0.3s; - z-index: 101; - display: flex; - flex-direction: column; + color: var(--text-secondary); + white-space: pre-wrap; + font-size: 0.85rem; } -.notifications-panel:not(.hidden) { - transform: translateX(0); +.dependency-onboarding-command-wrap { + margin-bottom: 10px; } -.notifications-content { - padding: var(--space-md); - overflow-y: auto; - flex: 1; +.dependency-onboarding-command-label { + margin: 0 0 6px; + font-size: 0.76rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-tertiary); } -.notification-list { +.dependency-setup-item-actions { display: flex; - flex-direction: column; - gap: var(--space-sm); + align-items: center; + gap: 8px; + flex-wrap: wrap; } -.notification-item { - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-tertiary); - padding: var(--space-sm); - cursor: pointer; - transition: border-color 0.15s, background 0.15s; +.dependency-setup-item-actions .btn-secondary { + width: auto; + min-height: 32px; + flex: 0 0 auto; } -.notification-item:hover { - border-color: rgba(56, 139, 253, 0.55); - background: rgba(56, 139, 253, 0.06); +.dependency-setup-item-actions a.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; } -.notification-item.unread { - border-left: 4px solid var(--accent-primary); +.dependency-setup-actions { + justify-content: flex-start; + flex-wrap: wrap; + gap: 8px; } -.notification-type { - font-size: 0.75rem; - padding: 2px 6px; - border-radius: 999px; - border: 1px solid var(--border-color); +.dependency-setup-actions .btn-secondary { + flex: 0 0 auto; +} + +.dependency-setup-empty { + padding: 12px; + border: 1px dashed var(--border-color); + border-radius: var(--radius-sm); color: var(--text-secondary); } -.notification-type.waiting { - border-color: rgba(245, 158, 11, 0.45); - color: var(--accent-warning); +.dependency-onboarding-progress { + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-tertiary); + padding: 12px; } -.notification-type.completed { - border-color: rgba(34, 197, 94, 0.45); - color: var(--accent-success); +.dependency-onboarding-progress-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; + color: var(--text-secondary); } -.notification-type.error { - border-color: rgba(239, 68, 68, 0.45); - color: var(--accent-danger); +.dependency-onboarding-progress-track { + width: 100%; + height: 8px; + border-radius: 999px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + overflow: hidden; } -.notification-message { - color: var(--text-primary); - line-height: 1.35; +.dependency-onboarding-progress-bar { + height: 100%; + background: var(--accent-primary); + transition: width 180ms ease-in-out; } -.notification-actions { - display: flex; - gap: var(--space-xs); - margin-top: var(--space-sm); - justify-content: flex-end; +.dependency-onboarding-step { + margin-top: 12px; } -.notification-action { - padding: 4px 8px; - font-size: 0.8rem; - width: auto; +.dependency-onboarding-step-card { + background: + linear-gradient(150deg, rgba(56, 139, 253, 0.09), rgba(34, 197, 94, 0.03)), + var(--bg-tertiary); } -.terminal-controls { - display: flex; - align-items: center; - gap: var(--space-sm); +.dependency-onboarding-step-kicker { + margin-bottom: 6px; + font-size: 0.74rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-tertiary); } -.terminal-controls label { - font-size: 0.9em; - margin: 0; +.dependency-onboarding-state { + margin: 0 0 8px; + color: var(--text-secondary); + font-weight: 500; } -/* Loading spinner for build button */ -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } +.dependency-onboarding-state.status-ok { + color: var(--accent-success); } -/* Server Launch Settings */ -.server-launch-group { - display: inline-flex; - gap: 4px; - align-items: center; +.dependency-onboarding-state.status-missing { + color: var(--accent-warning); } -#launch-settings-modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--overlay-backdrop); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; +.dependency-onboarding-state.status-pending { + color: var(--accent-primary); } -#launch-settings-modal .modal-content { - background: var(--bg-primary); - border-radius: var(--radius-lg); - max-width: 800px; - width: 90%; - max-height: 90vh; - overflow-y: auto; - box-shadow: var(--shadow-modal); +.dependency-gh-login-helper { + margin: 0 0 10px; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: + linear-gradient(145deg, rgba(56, 139, 253, 0.1), rgba(56, 139, 253, 0.02)), + var(--bg-tertiary); } -#launch-settings-modal .modal-header { - padding: var(--space-lg); - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; +.dependency-git-identity-helper { + margin: 0 0 10px; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: + linear-gradient(145deg, rgba(34, 197, 94, 0.08), rgba(34, 197, 94, 0.02)), + var(--bg-tertiary); } -#launch-settings-modal .modal-header h2 { - margin: 0; - font-size: 1.5rem; - color: var(--text-primary); +.dependency-git-identity-fields { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 8px; + margin-bottom: 8px; } -#launch-settings-modal .close-btn { - background: transparent; - border: none; - color: var(--text-secondary); - font-size: 1.5rem; - cursor: pointer; - padding: 0; - width: 32px; - height: 32px; +.dependency-git-identity-field { display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm); + flex-direction: column; + gap: 4px; + color: var(--text-secondary); + font-size: 0.86rem; } -#launch-settings-modal .close-btn:hover { +.dependency-git-identity-field input { + width: 100%; + min-height: 34px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); background: var(--bg-secondary); + color: var(--text-primary); + padding: 0 10px; + font-size: 0.9rem; } -#launch-settings-modal .modal-body { - padding: var(--space-lg); -} - -#launch-settings-modal .settings-section { - margin-bottom: var(--space-xl); -} - -#launch-settings-modal .settings-section:last-child { - margin-bottom: 0; -} - -#launch-settings-modal .settings-section h3 { - color: var(--accent-primary); - font-size: 1.1rem; - margin-bottom: var(--space-md); +.dependency-git-identity-field input:focus { + outline: none; + border-color: var(--accent-primary); } -#launch-settings-modal .setting-group { - margin-bottom: var(--space-md); +.dependency-git-identity-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; } -#launch-settings-modal .setting-group label { - display: block; - color: var(--text-secondary); - font-size: 0.9rem; - margin-bottom: var(--space-xs); +.dependency-git-help-btn { + min-width: 32px; + width: 32px; + padding: 0; + font-weight: 700; } -#launch-settings-modal .setting-group input { - width: 100%; - padding: var(--space-sm) var(--space-md); - background: var(--bg-secondary); +.dependency-git-help-inline { + margin-top: 8px; + padding: 10px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: monospace; + background: var(--bg-secondary); } -#launch-settings-modal .setting-group input:focus { - outline: none; - border-color: var(--accent-primary); +.dependency-git-help-line { + color: var(--text-secondary); + line-height: 1.45; } -#launch-settings-modal .setting-group small { - display: block; - color: var(--text-tertiary); - font-size: 0.8rem; - margin-top: var(--space-xs); +.dependency-git-help-line + .dependency-git-help-line { + margin-top: 6px; } -#launch-settings-modal .preset-checkboxes { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-md); +.dependency-gh-login-helper-text { + color: var(--text-secondary); + margin-bottom: 8px; } -#launch-settings-modal .preset-checkbox { +.dependency-gh-login-code-wrap { display: flex; align-items: center; - padding: var(--space-md); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; - user-select: none; -} - -#launch-settings-modal .preset-checkbox:hover { - background: var(--bg-tertiary); - border-color: var(--accent-primary); + gap: 8px; + flex-wrap: wrap; } -#launch-settings-modal .preset-checkbox input[type="checkbox"] { - margin-right: var(--space-sm); - width: 18px; - height: 18px; - cursor: pointer; +.dependency-gh-login-helper-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 8px; } -#launch-settings-modal .preset-checkbox input[type="checkbox"]:checked + span { - color: var(--accent-primary); +.dependency-gh-login-code { + display: inline-block; + padding: 7px 10px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: var(--bg-secondary); + color: var(--text-primary); + letter-spacing: 0.06em; font-weight: 600; } -#launch-settings-modal .preset-checkbox span { - font-size: 0.9rem; - transition: all 0.2s; +.dependency-setup-item-output { + max-height: 150px; + overflow: auto; } -#launch-settings-modal .modal-footer { - padding: var(--space-lg); - border-top: 1px solid var(--border-color); +.dependency-onboarding-nav { display: flex; + align-items: center; justify-content: flex-end; - gap: var(--space-md); + gap: 8px; + margin-top: 12px; } -#launch-settings-modal .btn-save, -#launch-settings-modal .btn-cancel { - padding: var(--space-sm) var(--space-lg); - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 0.9rem; - transition: all 0.2s; +.dependency-onboarding-nav .btn-secondary, +.dependency-onboarding-nav .btn-primary { + width: auto; + min-height: 34px; + flex: 0 0 auto; } -#launch-settings-modal .btn-save { - background: var(--accent-primary); - color: white; - border: none; -} +@media (max-width: 700px) { + body.dependency-onboarding-active #dependency-setup-modal { + padding: 14px; + } -#launch-settings-modal .btn-save:hover { - background: var(--accent-secondary); -} + .dependency-setup-content { + width: min(760px, 96vw); + } -#launch-settings-modal .btn-cancel { - background: var(--bg-secondary); - color: var(--text-secondary); - border: 1px solid var(--border-color); + .dependency-setup-actions { + justify-content: flex-start; + } + + .dependency-onboarding-nav { + justify-content: flex-end; + flex-wrap: wrap; + } } -#launch-settings-modal .btn-cancel:hover { - background: var(--bg-tertiary); +.settings-toolbar input { + flex: 1 1 auto; + min-width: 0; + padding: 0 10px; } -.loading-spinner { - display: inline-block; - width: 14px; - height: 14px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-top: 2px solid #fff; - border-radius: 50%; - animation: spin 1s linear infinite; +.settings-toolbar select { + flex: 0 0 auto; + min-width: 120px; + padding: 0 8px; } -.control-btn.building { - background: var(--color-warning); - cursor: not-allowed; - opacity: 0.8; +.settings-filter-hidden { + display: none !important; } -.clear-override-btn { - background: var(--bg-primary); - color: var(--text-secondary); +/* Review Console */ +.review-console-warning { + margin: 10px 12px 14px; + padding: 10px 12px; border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - padding: var(--space-xs); - cursor: pointer; - font-size: 0.8em; - transition: background-color 0.2s; + border-left: 4px solid var(--accent-warning); + background: rgba(210, 153, 34, 0.10); + border-radius: 10px; } -.clear-override-btn:hover { - background: var(--bg-tertiary); - color: var(--text-primary); +.setting-group { + margin-bottom: var(--space-md); } -.template-actions { +.setting-group label { display: flex; + align-items: center; gap: var(--space-sm); - margin-top: var(--space-sm); + cursor: pointer; } -.template-btn { - padding: var(--space-sm) var(--space-md); +.setting-group select { + background: var(--bg-tertiary); + color: var(--text-primary); border: 1px solid var(--border-color); + padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-sm); - cursor: pointer; - font-size: 0.9em; - transition: all 0.2s; + margin-left: var(--space-sm); } -.template-btn.primary { - background: var(--accent-primary); - color: white; - border-color: var(--accent-primary); +.setting-section { + margin: var(--space-lg) 0; + padding-top: var(--space-lg); + border-top: 1px solid var(--border-color); } -.template-btn.primary:hover { - background: var(--accent-primary-hover); - border-color: var(--accent-primary-hover); +.setting-section h4 { + color: var(--text-primary); + margin-bottom: var(--space-md); + font-size: 1.1em; } -.template-btn.secondary { - background: var(--bg-primary); +.setting-section h5 { color: var(--text-secondary); - border-color: var(--border-color); + margin-bottom: var(--space-sm); + font-size: 0.9em; + font-weight: 600; } -.template-btn.secondary:hover { - background: var(--bg-tertiary); - color: var(--text-primary); +.setting-group label small { + display: block; + color: var(--text-tertiary); + font-size: 0.8em; + margin-top: var(--space-xs); + margin-left: var(--space-lg); } -.update-notification { - margin-top: var(--space-sm); - padding: var(--space-sm); - background: var(--bg-tertiary); - border: 1px solid var(--accent-warning); +.setting-description { + color: var(--text-tertiary); + font-size: 0.85em; + margin-bottom: var(--space-md); +} + +.settings-glossary details { + border: 1px solid var(--border-color); border-radius: var(--radius-sm); - transition: all 0.3s; + padding: 8px 10px; + background: var(--bg-tertiary); + margin: 10px 0; } -.update-notification.hidden { - display: none; +.settings-glossary details[open] { + background: rgba(255, 255, 255, 0.02); } -.notification-content { +.settings-glossary summary { + cursor: pointer; display: flex; align-items: center; - gap: var(--space-sm); + gap: 8px; + list-style: none; } -.notification-icon { - font-size: 1.1em; +.settings-glossary summary::-webkit-details-marker { + display: none; } -.notification-text { - flex: 1; - color: var(--text-primary); - font-size: 0.9em; +.settings-glossary details > .setting-description { + margin: 10px 0 0; + opacity: 0.95; } -.dismiss-btn { - background: none; +.identity-saved-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.identity-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.03); + font-family: var(--font-mono); + font-size: 0.78rem; + max-width: 100%; +} + +.identity-chip button { border: none; - color: var(--text-secondary); + background: transparent; + color: var(--text-tertiary); cursor: pointer; - font-size: 1.2em; padding: 0; - width: 24px; - height: 24px; + line-height: 1; +} + +.identity-chip button:hover { + color: var(--text-secondary); +} + +.per-terminal-item { display: flex; align-items: center; - justify-content: center; + justify-content: space-between; + padding: var(--space-sm); + margin-bottom: var(--space-xs); + background: var(--bg-tertiary); border-radius: var(--radius-sm); - transition: all 0.2s; } -.dismiss-btn:hover { - background: var(--bg-primary); +.per-terminal-item .terminal-name { color: var(--text-primary); + font-weight: 500; + font-family: var(--font-mono); } -/* Loading */ -.loading-message { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; +.per-terminal-item .terminal-override { + color: var(--text-secondary); + font-size: 0.8em; } -.spinner { - width: 40px; - height: 40px; - border: 3px solid var(--bg-tertiary); - border-top-color: var(--accent-primary); - border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto var(--space-md); +.per-terminal-items { + margin-top: var(--space-sm); } -@keyframes spin { - to { transform: rotate(360deg); } +/* Notifications Panel */ +.notifications-panel { + position: fixed; + top: var(--header-height); + right: 0; + bottom: 0; + width: min(420px, 96vw); + background: + linear-gradient(180deg, rgba(18, 24, 38, 0.94), rgba(10, 15, 27, 0.94)); + border-left: 1px solid rgba(147, 197, 253, 0.22); + box-shadow: -20px 0 44px rgba(2, 6, 16, 0.58); + backdrop-filter: blur(12px); + transform: translateX(100%); + transition: transform 0.24s ease; + z-index: 12040; + display: flex; + flex-direction: column; } -@keyframes pulse { - from { opacity: 0.5; } - to { opacity: 1; } +.notifications-panel:not(.hidden) { + transform: translateX(0); } -/* Toast Notifications */ -.ready-toast { - position: fixed; - top: calc(var(--header-height) + 20px); - right: 20px; - background: var(--accent-success); - color: white; - padding: var(--space-sm) var(--space-md); - border-radius: var(--radius-md); - box-shadow: var(--shadow-soft); - z-index: 1000; - animation: slideInRight 0.3s ease-out, fadeOutRight 0.3s ease-in 2.7s forwards; +.notifications-panel .panel-header { + padding: 14px 16px; + border-bottom: 1px solid rgba(147, 197, 253, 0.2); + background: + linear-gradient(180deg, rgba(35, 51, 74, 0.64), rgba(26, 38, 58, 0.38)); } -.toast-content { - display: flex; - align-items: center; - gap: var(--space-sm); +.notifications-panel .panel-header h3 { + margin: 0; + font-size: 1rem; + font-weight: 640; + letter-spacing: 0.01em; + color: #f4f8ff; } -.toast-icon { - font-size: 1.2rem; +.notifications-panel .panel-actions .icon-button { + width: 28px; + height: 28px; + border-radius: 8px; + border: 1px solid rgba(147, 197, 253, 0.24); + background: rgba(7, 12, 22, 0.56); + color: rgba(244, 248, 255, 0.9); } -.toast-text { - font-weight: 500; - font-size: 0.875rem; +.notifications-panel .panel-actions .icon-button:hover { + border-color: rgba(147, 197, 253, 0.44); + background: rgba(29, 78, 216, 0.2); } -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } +.notifications-content { + padding: 12px 14px 18px; + overflow-y: auto; + overscroll-behavior: contain; + flex: 1; } -@keyframes fadeOutRight { - from { - transform: translateX(0); - opacity: 1; - } - to { - transform: translateX(100%); - opacity: 0; - } +.notification-list { + display: flex; + flex-direction: column; + gap: 10px; } -/* Responsive - ONLY apply if data-visible-count is not set */ -@media (max-width: 1200px) { - .terminal-grid:not([data-visible-count]) { - grid-template-columns: repeat(2, 1fr); - } +.notifications-panel .empty-message { + border: 1px dashed rgba(147, 197, 253, 0.24); + border-radius: 12px; + background: rgba(12, 19, 33, 0.62); + color: rgba(223, 231, 245, 0.76); } -@media (max-width: 768px) { - .projects-chats-shell { - grid-template-columns: 1fr; - min-height: 70vh; - } +.notifications-panel .notification-item { + border: 1px solid rgba(147, 197, 253, 0.2); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(17, 25, 40, 0.92), rgba(10, 16, 28, 0.92)); + padding: 12px 12px 10px; + cursor: pointer; + transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease; + box-shadow: 0 8px 18px rgba(2, 6, 16, 0.34); +} - .sidebar-toggle { - display: inline-flex; - } +.notifications-panel .notification-item:hover { + transform: translateY(-1px); + border-color: rgba(96, 165, 250, 0.5); + box-shadow: 0 12px 24px rgba(9, 30, 66, 0.42); +} - header { - padding: 0 var(--space-md); - } +.notifications-panel .notification-item.unread { + border-left: 3px solid #60a5fa; + box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.22), 0 12px 24px rgba(9, 30, 66, 0.38); +} - .header-content { - gap: var(--space-md); - } +.notifications-panel .notification-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 7px; + gap: 8px; +} + +.notifications-panel .notification-time { + font-size: 0.75rem; + letter-spacing: 0.01em; + color: rgba(223, 231, 245, 0.66); +} + +.notifications-panel .notification-type { + font-size: 0.69rem; + font-weight: 620; + letter-spacing: 0.055em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 999px; + border: 1px solid rgba(148, 163, 184, 0.3); + background: rgba(15, 23, 42, 0.65); + color: rgba(226, 232, 240, 0.9); +} + +.notifications-panel .notification-type.waiting { + border-color: rgba(245, 158, 11, 0.45); + background: rgba(245, 158, 11, 0.14); + color: #fcd34d; +} + +.notifications-panel .notification-type.completed { + border-color: rgba(34, 197, 94, 0.45); + background: rgba(34, 197, 94, 0.14); + color: #86efac; +} + +.notifications-panel .notification-type.error { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(239, 68, 68, 0.12); + color: #fca5a5; +} + +.notifications-panel .notification-message { + color: rgba(239, 245, 255, 0.94); + line-height: 1.42; + font-size: 0.89rem; + margin: 0; +} + +.notifications-panel .notification-meta { + font-size: 0.75rem; + color: rgba(190, 206, 230, 0.72); + margin-top: 6px; +} + +.notifications-panel .notification-actions { + display: flex; + gap: 8px; + margin-top: 10px; + justify-content: flex-end; +} + +.notifications-panel .notification-action { + min-width: 0; + width: auto; + border-radius: 8px; + border: 1px solid rgba(147, 197, 253, 0.24); + background: rgba(13, 20, 35, 0.6); + color: rgba(239, 245, 255, 0.9); + padding: 4px 10px; + font-size: 0.78rem; +} + +.notifications-panel .notification-action:hover { + border-color: rgba(147, 197, 253, 0.45); + background: rgba(37, 99, 235, 0.24); +} + +.terminal-controls { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.terminal-controls label { + font-size: 0.9em; + margin: 0; +} + +/* Loading spinner for build button */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Server Launch Settings */ +.server-launch-group { + display: inline-flex; + gap: 4px; + align-items: center; +} + +#launch-settings-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--overlay-backdrop); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; +} + +#launch-settings-modal .modal-content { + background: var(--bg-primary); + border-radius: var(--radius-lg); + max-width: 800px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-modal); +} + +#launch-settings-modal .modal-header { + padding: var(--space-lg); + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +#launch-settings-modal .modal-header h2 { + margin: 0; + font-size: 1.5rem; + color: var(--text-primary); +} + +#launch-settings-modal .close-btn { + background: transparent; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); +} + +#launch-settings-modal .close-btn:hover { + background: var(--bg-secondary); +} + +#launch-settings-modal .modal-body { + padding: var(--space-lg); +} + +#launch-settings-modal .settings-section { + margin-bottom: var(--space-xl); +} + +#launch-settings-modal .settings-section:last-child { + margin-bottom: 0; +} + +#launch-settings-modal .settings-section h3 { + color: var(--accent-primary); + font-size: 1.1rem; + margin-bottom: var(--space-md); +} + +#launch-settings-modal .setting-group { + margin-bottom: var(--space-md); +} + +#launch-settings-modal .setting-group label { + display: block; + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: var(--space-xs); +} + +#launch-settings-modal .setting-group input { + width: 100%; + padding: var(--space-sm) var(--space-md); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: monospace; +} + +#launch-settings-modal .setting-group input:focus { + outline: none; + border-color: var(--accent-primary); +} + +#launch-settings-modal .setting-group small { + display: block; + color: var(--text-tertiary); + font-size: 0.8rem; + margin-top: var(--space-xs); +} + +#launch-settings-modal .preset-checkboxes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--space-md); +} + +#launch-settings-modal .preset-checkbox { + display: flex; + align-items: center; + padding: var(--space-md); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.2s; + user-select: none; +} + +#launch-settings-modal .preset-checkbox:hover { + background: var(--bg-tertiary); + border-color: var(--accent-primary); +} + +#launch-settings-modal .preset-checkbox input[type="checkbox"] { + margin-right: var(--space-sm); + width: 18px; + height: 18px; + cursor: pointer; +} + +#launch-settings-modal .preset-checkbox input[type="checkbox"]:checked + span { + color: var(--accent-primary); + font-weight: 600; +} + +#launch-settings-modal .preset-checkbox span { + font-size: 0.9rem; + transition: all 0.2s; +} + +#launch-settings-modal .modal-footer { + padding: var(--space-lg); + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: var(--space-md); +} + +#launch-settings-modal .btn-save, +#launch-settings-modal .btn-cancel { + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s; +} + +#launch-settings-modal .btn-save { + background: var(--accent-primary); + color: white; + border: none; +} + +#launch-settings-modal .btn-save:hover { + background: var(--accent-secondary); +} + +#launch-settings-modal .btn-cancel { + background: var(--bg-secondary); + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +#launch-settings-modal .btn-cancel:hover { + background: var(--bg-tertiary); +} + +.loading-spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top: 2px solid #fff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.control-btn.building { + background: var(--color-warning); + cursor: not-allowed; + opacity: 0.8; +} + +.clear-override-btn { + background: var(--bg-primary); + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: var(--space-xs); + cursor: pointer; + font-size: 0.8em; + transition: background-color 0.2s; +} + +.clear-override-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.template-actions { + display: flex; + gap: var(--space-sm); + margin-top: var(--space-sm); +} + +.template-btn { + padding: var(--space-sm) var(--space-md); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.9em; + transition: all 0.2s; +} + +.template-btn.primary { + background: var(--accent-primary); + color: white; + border-color: var(--accent-primary); +} + +.template-btn.primary:hover { + background: var(--accent-primary-hover); + border-color: var(--accent-primary-hover); +} + +.template-btn.secondary { + background: var(--bg-primary); + color: var(--text-secondary); + border-color: var(--border-color); +} + +.template-btn.secondary:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.update-notification { + margin-top: var(--space-sm); + padding: var(--space-sm); + background: var(--bg-tertiary); + border: 1px solid var(--accent-warning); + border-radius: var(--radius-sm); + transition: all 0.3s; +} + +.update-notification.hidden { + display: none; +} + +.notification-content { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.notification-icon { + font-size: 1.1em; +} + +.notification-text { + flex: 1; + color: var(--text-primary); + font-size: 0.9em; +} + +.dismiss-btn { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 1.2em; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + transition: all 0.2s; +} + +.dismiss-btn:hover { + background: var(--bg-primary); + color: var(--text-primary); +} + +/* Loading */ +.loading-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--bg-tertiary); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto var(--space-md); +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes pulse { + from { opacity: 0.5; } + to { opacity: 1; } +} + +/* Toast Notifications */ +.toast-stack { + position: fixed; + top: calc(var(--header-height) + 20px); + right: 20px; + width: min(420px, calc(100vw - 24px)); + display: flex; + flex-direction: column; + gap: 10px; + z-index: 12060; + pointer-events: none; +} + +.toast, +.ready-toast { + --toast-accent: 96, 165, 250; + pointer-events: auto; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + border-radius: 14px; + border: 1px solid rgba(var(--toast-accent), 0.42); + background: + linear-gradient(180deg, rgba(17, 25, 40, 0.95), rgba(9, 14, 24, 0.95)); + box-shadow: 0 14px 32px rgba(2, 6, 16, 0.5); + color: rgba(244, 248, 255, 0.96); + padding: 11px 12px; + transform: translateX(20px); + opacity: 0; + transition: opacity 0.22s ease, transform 0.22s ease; +} + +.toast.is-visible, +.ready-toast.is-visible { + transform: translateX(0); + opacity: 1; +} + +.toast.is-leaving, +.ready-toast.is-leaving { + transform: translateX(16px); + opacity: 0; +} + +.toast.toast-success, +.ready-toast { + --toast-accent: 74, 222, 128; +} + +.toast.toast-warning { + --toast-accent: 251, 191, 36; +} + +.toast.toast-error { + --toast-accent: 248, 113, 113; +} + +.toast-content { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; + flex: 1; +} + +.toast-icon { + width: 24px; + height: 24px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(var(--toast-accent), 0.2); + color: rgb(var(--toast-accent)); + flex: 0 0 auto; +} + +.toast-icon svg { + width: 14px; + height: 14px; + display: block; +} + +.toast-text { + font-weight: 500; + font-size: 0.88rem; + line-height: 1.38; + color: rgba(242, 247, 255, 0.95); + word-break: break-word; +} + +.toast-close { + width: 22px; + height: 22px; + border-radius: 7px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(15, 23, 42, 0.58); + color: rgba(230, 237, 249, 0.84); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.72rem; + line-height: 1; + padding: 0; + flex: 0 0 auto; +} + +.toast-close:hover { + background: rgba(51, 65, 85, 0.62); + color: rgba(255, 255, 255, 0.97); + border-color: rgba(255, 255, 255, 0.3); +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes fadeOutRight { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +/* Responsive - ONLY apply if data-visible-count is not set */ +@media (max-width: 1200px) { + .terminal-grid:not([data-visible-count]) { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .projects-chats-shell { + grid-template-columns: 1fr; + min-height: 70vh; + } + + .sidebar-toggle { + display: inline-flex; + } + + header { + padding: 0 var(--space-md); + } + + .header-content { + gap: var(--space-md); + } header h1 { font-size: 1rem; @@ -7438,25 +8201,26 @@ header h1 { top: calc(var(--header-height) + 20px); right: 20px; width: 300px; - z-index: 1001; + z-index: 12050; pointer-events: none; } .cross-workspace-notification { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-left: 4px solid var(--accent-warning); - border-radius: var(--radius-md); + background: + linear-gradient(180deg, rgba(18, 24, 38, 0.94), rgba(10, 15, 27, 0.94)); + border: 1px solid rgba(245, 158, 11, 0.28); + border-left: 3px solid rgba(245, 158, 11, 0.85); + border-radius: 12px; margin-bottom: var(--space-md); - box-shadow: var(--shadow-soft); + box-shadow: 0 14px 28px rgba(2, 6, 16, 0.42); pointer-events: auto; - animation: slideInRight 0.3s ease-out; + animation: slideInRight 0.24s ease-out; } -.notification-header { +.cross-workspace-notification .notification-header { padding: var(--space-sm) var(--space-md); - background: var(--bg-tertiary); - border-bottom: 1px solid var(--border-color); + background: rgba(245, 158, 11, 0.1); + border-bottom: 1px solid rgba(245, 158, 11, 0.24); display: flex; justify-content: space-between; align-items: center; @@ -7468,7 +8232,7 @@ header h1 { color: var(--text-primary); } -.notification-close { +.cross-workspace-notification .notification-close { background: none; border: none; color: var(--text-secondary); @@ -7481,17 +8245,17 @@ header h1 { justify-content: center; } -.notification-body { +.cross-workspace-notification .notification-body { padding: var(--space-md); } -.notification-message { +.cross-workspace-notification .notification-message { font-size: 0.9rem; color: var(--text-primary); margin-bottom: var(--space-md); } -.notification-actions { +.cross-workspace-notification .notification-actions { display: flex; gap: var(--space-sm); } @@ -9260,395 +10024,661 @@ header h1 { font-size: 0.75rem; } -.detail-row-full { +.detail-row-full { + display: flex; + gap: var(--space-sm); + align-items: flex-start; +} + +.detail-row-full .detail-label { + color: var(--text-muted); + flex-shrink: 0; + min-width: 80px; +} + +.folder-path-full { + color: #90cdf4; + word-break: break-all; + font-family: monospace; + font-size: 0.75rem; + background: rgba(99, 179, 237, 0.1); + padding: 4px 8px; + border-radius: 4px; +} + +/* Conversation details grid */ +.conv-details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--space-xs) var(--space-md); + margin: var(--space-sm) 0; + padding: var(--space-sm); + background: var(--bg-primary); + border-radius: var(--radius-xs); + font-size: 0.75rem; +} + +.detail-row { + display: flex; + gap: var(--space-xs); + align-items: baseline; +} + +.detail-label { + color: var(--text-muted); + min-width: 60px; + flex-shrink: 0; +} + +.detail-value { + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.detail-value.folder-path { + color: #63b3ed; + cursor: help; +} + +.detail-value.repo-name { + color: #9f7aea; +} + +.detail-value.branch-name { + color: #48bb78; +} + +.detail-value.model-name { + color: #f6ad55; +} + +.conv-actions { + display: flex; + gap: var(--space-sm); + flex-wrap: wrap; + margin-top: var(--space-sm); +} + +.conv-actions .btn-small { + padding: 6px 12px; + font-size: 0.8rem; +} + +.conv-actions .btn-small.primary { + background: var(--accent-primary); + color: var(--text-on-accent); + border: none; +} + +.conv-actions .btn-small.primary:hover { + background: var(--accent-primary-hover); +} + +.conv-actions .btn-small.secondary { + background: var(--bg-primary); + border: 1px solid var(--border-color); + color: var(--text-secondary); +} + +.conv-actions .btn-small.secondary:hover { + background: var(--bg-tertiary); +} + +.browser-footer { + padding: var(--space-sm) var(--space-lg); + border-top: 1px solid var(--border-color); + font-size: 0.8rem; + color: var(--text-muted); +} + +.no-results, .loading, .error { + text-align: center; + padding: var(--space-xl); + color: var(--text-muted); +} + +/* Conversation Details Modal */ +.conversation-details-modal .details-content { + max-width: 700px; + width: 90vw; + max-height: 85vh; + display: flex; + flex-direction: column; +} + +.details-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-md) var(--space-lg); + border-bottom: 1px solid var(--border-color); +} + +.details-meta { + padding: var(--space-md) var(--space-lg); + background: var(--bg-tertiary); + font-size: 0.85rem; +} + +.details-meta p { + margin: var(--space-xs) 0; +} + +.details-summary { + padding: var(--space-md) var(--space-lg); + border-bottom: 1px solid var(--border-color); + font-style: italic; + color: var(--text-secondary); +} + +.details-messages { + flex: 1; + overflow-y: auto; + padding: var(--space-md) var(--space-lg); +} + +.details-messages h4 { + margin-bottom: var(--space-md); +} + +.messages-list { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.messages-list .message { + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + font-size: 0.85rem; +} + +.messages-list .message.user { + background: var(--bg-tertiary); + margin-left: var(--space-lg); +} + +.messages-list .message.assistant { + background: var(--accent-primary); + background: rgba(59, 130, 246, 0.2); + margin-right: var(--space-lg); +} + +.message-header { + display: flex; + justify-content: space-between; + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: var(--space-xs); +} + +.message-content { + white-space: pre-wrap; + word-break: break-word; +} + +.tool-uses { + margin-top: var(--space-xs); + font-size: 0.7rem; + color: var(--text-muted); +} + +.more-messages { + text-align: center; + padding: var(--space-md); + color: var(--text-muted); + font-style: italic; +} + +.details-actions { + padding: var(--space-md) var(--space-lg); + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; +} + +/* ============================================ + Ports Panel Styles + ============================================ */ + +.ports-modal .ports-content { + max-width: 1100px; + width: 96vw; + max-height: 92vh; + display: flex; + flex-direction: column; +} + +.ports-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-md) var(--space-lg); + border-bottom: 1px solid var(--border-color); +} + +.ports-header h2 { + margin: 0; + font-size: 1.2rem; +} + +.ports-info { + padding: var(--space-sm) var(--space-lg); + background: var(--bg-tertiary); + font-size: 0.85rem; + color: var(--text-muted); +} + +.ports-list { + flex: 1; + overflow-y: auto; + padding: var(--space-lg); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: var(--space-md); + align-content: start; +} + +.port-item { display: flex; + flex-direction: column; gap: var(--space-sm); - align-items: flex-start; + padding: var(--space-md); + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + border-left: 3px solid var(--border-color); + transition: all 0.15s; } -.detail-row-full .detail-label { - color: var(--text-muted); - flex-shrink: 0; - min-width: 80px; +.port-item:hover { + background: var(--bg-primary); } -.folder-path-full { - color: #90cdf4; - word-break: break-all; - font-family: monospace; - font-size: 0.75rem; - background: rgba(99, 179, 237, 0.1); - padding: 4px 8px; - border-radius: 4px; +.port-item.orchestrator, +.port-item.orchestrator-dev { + border-left-color: #9f7aea; } -/* Conversation details grid */ -.conv-details-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-xs) var(--space-md); - margin: var(--space-sm) 0; - padding: var(--space-sm); - background: var(--bg-primary); - border-radius: var(--radius-xs); - font-size: 0.75rem; +.port-item.client, +.port-item.client-dev { + border-left-color: #63b3ed; } -.detail-row { - display: flex; - gap: var(--space-xs); - align-items: baseline; +.port-item.vite, +.port-item.react { + border-left-color: #48bb78; } -.detail-label { - color: var(--text-muted); - min-width: 60px; - flex-shrink: 0; +.port-item.game-server { + border-left-color: #f6ad55; } -.detail-value { - color: var(--text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.port-item.python, +.port-item.flask { + border-left-color: #ffd93d; } -.detail-value.folder-path { - color: #63b3ed; - cursor: help; +.port-item.node { + border-left-color: #68d391; } -.detail-value.repo-name { - color: #9f7aea; +.port-icon { + font-size: 1.2rem; + flex-shrink: 0; } -.detail-value.branch-name { - color: #48bb78; +.port-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-md); + min-width: 0; } -.detail-value.model-name { - color: #f6ad55; +.port-main { + display: flex; + align-items: flex-start; + gap: var(--space-md); + min-width: 0; + flex: 1; } -.conv-actions { - display: flex; - gap: var(--space-sm); - flex-wrap: wrap; - margin-top: var(--space-sm); +.port-details { + flex: 1; + min-width: 0; } -.conv-actions .btn-small { - padding: 6px 12px; - font-size: 0.8rem; +.port-name { + display: block; + font-weight: 500; + color: var(--text-primary); + font-size: 0.9rem; } -.conv-actions .btn-small.primary { - background: var(--accent-primary); - color: var(--text-on-accent); - border: none; +.port-process { + font-size: 0.75rem; + color: var(--text-muted); } -.conv-actions .btn-small.primary:hover { - background: var(--accent-primary-hover); +.port-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-xs); + flex-wrap: wrap; } -.conv-actions .btn-small.secondary { - background: var(--bg-primary); +.port-action-btn { border: 1px solid var(--border-color); - color: var(--text-secondary); + background: var(--bg-primary); + color: var(--text-primary); + border-radius: var(--radius-xs); + padding: 4px 8px; + font-size: 0.75rem; + cursor: pointer; + line-height: 1.1; + transition: background 0.15s, border-color 0.15s; } -.conv-actions .btn-small.secondary:hover { - background: var(--bg-tertiary); +.port-action-btn:hover { + background: var(--bg-secondary); + border-color: var(--accent-primary); } -.browser-footer { - padding: var(--space-sm) var(--space-lg); +.ports-footer { + padding: var(--space-md) var(--space-lg); border-top: 1px solid var(--border-color); - font-size: 0.8rem; - color: var(--text-muted); + display: flex; + justify-content: flex-end; } -.no-results, .loading, .error { +.no-ports { text-align: center; - padding: var(--space-xl); + padding: var(--space-lg); color: var(--text-muted); } -/* Conversation Details Modal */ -.conversation-details-modal .details-content { - max-width: 700px; - width: 90vw; - max-height: 85vh; +/* ============================================ + PRs Panel Styles + ============================================ */ + +.prs-modal .prs-content { + max-width: 1100px; + width: 96vw; + max-height: 92vh; display: flex; flex-direction: column; } -.details-header { +.prs-toolbar { + padding: var(--space-sm) var(--space-lg); + border-bottom: 1px solid var(--border-color); + background: var(--bg-tertiary); display: flex; - justify-content: space-between; + gap: var(--space-md); align-items: center; - padding: var(--space-md) var(--space-lg); - border-bottom: 1px solid var(--border-color); + flex-wrap: wrap; } -.details-meta { - padding: var(--space-md) var(--space-lg); - background: var(--bg-tertiary); - font-size: 0.85rem; +.prs-toolbar-group { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; } -.details-meta p { - margin: var(--space-xs) 0; +.prs-label { + font-size: 0.75rem; + color: var(--text-secondary); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; } -.details-summary { - padding: var(--space-md) var(--space-lg); - border-bottom: 1px solid var(--border-color); - font-style: italic; - color: var(--text-secondary); +.prs-search { + min-width: 220px; + flex: 1; } -.details-messages { +.prs-input { + min-width: 220px; + flex: 0 1 320px; +} + +.prs-list { flex: 1; overflow-y: auto; - padding: var(--space-md) var(--space-lg); + padding: var(--space-lg); + display: flex; + flex-direction: column; + gap: var(--space-sm); } -.details-messages h4 { - margin-bottom: var(--space-md); +.pr-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-md); + padding: var(--space-md); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-secondary); } -.messages-list { +.pr-main { display: flex; flex-direction: column; - gap: var(--space-sm); + gap: 6px; + min-width: 0; + flex: 1; } -.messages-list .message { - padding: var(--space-sm) var(--space-md); - border-radius: var(--radius-sm); - font-size: 0.85rem; +.pr-title { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; } -.messages-list .message.user { - background: var(--bg-tertiary); - margin-left: var(--space-lg); +.pr-repo { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-secondary); } -.messages-list .message.assistant { - background: var(--accent-primary); - background: rgba(59, 130, 246, 0.2); - margin-right: var(--space-lg); +.pr-number { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-secondary); } -.message-header { - display: flex; - justify-content: space-between; +.pr-badge { + font-family: var(--font-mono); font-size: 0.75rem; - color: var(--text-muted); - margin-bottom: var(--space-xs); + padding: 3px 6px; + border-radius: 999px; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-secondary); } -.message-content { - white-space: pre-wrap; - word-break: break-word; +.pr-badge.draft { + border-color: var(--accent-warning); } -.tool-uses { - margin-top: var(--space-xs); - font-size: 0.7rem; - color: var(--text-muted); +.pr-subtitle { + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.more-messages { - text-align: center; - padding: var(--space-md); - color: var(--text-muted); - font-style: italic; +.pr-meta { + font-size: 0.75rem; + color: var(--text-tertiary); } -.details-actions { - padding: var(--space-md) var(--space-lg); - border-top: 1px solid var(--border-color); +.pr-actions { display: flex; - justify-content: flex-end; + gap: var(--space-sm); + flex-shrink: 0; } /* ============================================ - Ports Panel Styles + Tasks Panel Styles ============================================ */ -.ports-modal .ports-content { - max-width: 1100px; - width: 96vw; - max-height: 92vh; - display: flex; - flex-direction: column; -} - -.ports-header { +.queue-summary { display: flex; - justify-content: space-between; - align-items: center; - padding: var(--space-md) var(--space-lg); - border-bottom: 1px solid var(--border-color); -} - -.ports-header h2 { - margin: 0; - font-size: 1.2rem; -} - -.ports-info { - padding: var(--space-sm) var(--space-lg); - background: var(--bg-tertiary); - font-size: 0.85rem; - color: var(--text-muted); -} - -.ports-list { - flex: 1; - overflow-y: auto; - padding: var(--space-lg); - display: grid; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); - gap: var(--space-md); - align-content: start; + gap: 8px; + flex-wrap: wrap; + padding: 0 0 10px 0; } -.port-item { +.tasks-modal .tasks-content { + /* Tasks is a primary workflow: prefer a robust, full-viewport panel. */ + max-width: none; + box-sizing: border-box; + width: 100%; + max-width: 100vw; + max-height: 100vh; + height: 100%; display: flex; flex-direction: column; - gap: var(--space-sm); + overflow: hidden; + position: relative; + border-radius: 0; padding: var(--space-md); - background: var(--bg-tertiary); - border-radius: var(--radius-sm); - border-left: 3px solid var(--border-color); - transition: all 0.15s; + --tasks-board-accent: var(--accent-primary); + direction: ltr; + text-align: left; } -.port-item:hover { - background: var(--bg-primary); +.tasks-modal .tasks-content.tasks-has-board-accent .tasks-toolbar { + border-bottom-color: color-mix(in srgb, var(--border-color) 55%, var(--tasks-board-accent) 45%); } -.port-item.orchestrator, -.port-item.orchestrator-dev { - border-left-color: #9f7aea; +.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header { + background: color-mix(in srgb, var(--bg-tertiary) 86%, var(--tasks-board-accent) 14%); } -.port-item.client, -.port-item.client-dev { - border-left-color: #63b3ed; +.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header:hover { + background: color-mix(in srgb, var(--bg-primary) 86%, var(--tasks-board-accent) 14%); } -.port-item.vite, -.port-item.react { - border-left-color: #48bb78; +.tasks-modal .tasks-body { + direction: ltr; + text-align: left; } -.port-item.game-server { - border-left-color: #f6ad55; +.tasks-modal { + background: transparent; } -.port-item.python, -.port-item.flask { - border-left-color: #ffd93d; +.tasks-modal.tasks-theme-light { + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-tertiary: #e6e8eb; + --text-primary: #24292f; + --text-secondary: #57606a; + --text-tertiary: #6e7781; + --border-color: #d0d7de; } -.port-item.node { - border-left-color: #68d391; +.tasks-modal .modal-header { + position: sticky; + top: 0; + z-index: 5; + background: var(--bg-secondary); + padding-bottom: var(--space-sm); } -.port-icon { - font-size: 1.2rem; - flex-shrink: 0; +.tasks-modal .tasks-close-btn { + width: 44px; + height: 44px; + font-size: 1.6rem; + color: var(--accent-danger); + border: 1px solid rgba(248, 81, 73, 0.35); + background: rgba(248, 81, 73, 0.12); } -.port-card-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-md); - min-width: 0; +.tasks-modal .tasks-close-btn:hover { + background: rgba(248, 81, 73, 0.2); + border-color: rgba(248, 81, 73, 0.55); + color: var(--accent-danger); } -.port-main { - display: flex; - align-items: flex-start; - gap: var(--space-md); - min-width: 0; - flex: 1; +.tasks-modal .tasks-close-btn:focus-visible { + outline: 2px solid var(--accent-danger); + outline-offset: 2px; } -.port-details { - flex: 1; - min-width: 0; +.tasks-view-toggle { + display: flex; + gap: 6px; + align-items: center; } -.port-name { - display: block; - font-weight: 500; - color: var(--text-primary); - font-size: 0.9rem; +.tasks-view-btn.active { + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15); } -.port-process { - font-size: 0.75rem; - color: var(--text-muted); +.tasks-filter { + position: relative; } -.port-actions { - display: flex; - align-items: center; - justify-content: flex-end; - gap: var(--space-xs); - flex-wrap: wrap; +.tasks-filter summary { + list-style: none; } -.port-action-btn { - border: 1px solid var(--border-color); - background: var(--bg-primary); - color: var(--text-primary); - border-radius: var(--radius-xs); - padding: 4px 8px; - font-size: 0.75rem; - cursor: pointer; - line-height: 1.1; - transition: background 0.15s, border-color 0.15s; +.tasks-filter summary::-webkit-details-marker { + display: none; } -.port-action-btn:hover { +.tasks-filter-popover { + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 240px; + max-height: 320px; + overflow: auto; + z-index: 5; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); background: var(--bg-secondary); - border-color: var(--accent-primary); + box-shadow: 0 10px 24px rgba(0,0,0,0.12); + padding: 10px; } -.ports-footer { - padding: var(--space-md) var(--space-lg); - border-top: 1px solid var(--border-color); +.tasks-filter-actions { display: flex; - justify-content: flex-end; + gap: 8px; + margin-bottom: 10px; } -.no-ports { - text-align: center; - padding: var(--space-lg); - color: var(--text-muted); +.tasks-filter-list { + display: flex; + flex-direction: column; + gap: 6px; } -/* ============================================ - PRs Panel Styles - ============================================ */ - -.prs-modal .prs-content { - max-width: 1100px; - width: 96vw; - max-height: 92vh; +.tasks-filter-item { display: flex; - flex-direction: column; + align-items: center; + gap: 8px; + font-size: 0.85rem; + color: var(--text-primary); +} + +.tasks-filter-item input { + accent-color: var(--accent-primary); } -.prs-toolbar { +.tasks-toolbar { padding: var(--space-sm) var(--space-lg); border-bottom: 1px solid var(--border-color); background: var(--bg-tertiary); @@ -9658,201 +10688,255 @@ header h1 { flex-wrap: wrap; } -.prs-toolbar-group { - display: flex; - gap: 8px; +.tasks-launch-defaults { + display: inline-flex; align-items: center; - flex-wrap: wrap; + gap: 6px; + padding: 4px 8px; + border: 1px solid var(--border-color); + border-radius: 999px; + background: var(--bg-secondary); } -.prs-label { - font-size: 0.75rem; - color: var(--text-secondary); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; +.tasks-launch-default-tier-group { + padding: 0; + border: none; + background: transparent; } -.prs-search { - min-width: 220px; - flex: 1; +.tasks-launch-default-agent-group { + padding: 0; + border: none; + background: transparent; } -.prs-input { - min-width: 220px; - flex: 0 1 320px; +.tasks-launch-default-mode-group { + padding: 0; + border: none; + background: transparent; } -.prs-list { - flex: 1; - overflow-y: auto; - padding: var(--space-lg); - display: flex; - flex-direction: column; - gap: var(--space-sm); +.tasks-launch-defaults-label { + font-weight: 900; + font-size: 0.85rem; + color: var(--text-secondary); } -.pr-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-md); - padding: var(--space-md); +.tasks-toggle.tasks-toggle-mini { + font-size: 0.75rem; + gap: 6px; +} + +.tasks-toggle.tasks-toggle-mini span { + opacity: 0.9; +} + +.tasks-board-accent { + width: 12px; + height: 12px; + border-radius: 999px; + background: var(--tasks-board-accent); border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); + box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.12); } -.pr-main { - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; - flex: 1; +.tasks-board-accent.is-hidden { + display: none; } -.pr-title { - display: flex; +.tasks-board-picker { + position: relative; + display: inline-flex; align-items: center; gap: 8px; - flex-wrap: wrap; } -.pr-repo { - font-family: var(--font-mono); - font-size: 0.8rem; - color: var(--text-secondary); +.tasks-board-btn { + min-width: 200px; + text-align: left; + justify-content: flex-start; } -.pr-number { - font-family: var(--font-mono); - font-size: 0.8rem; - color: var(--text-secondary); +.tasks-select-hidden { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; } -.pr-badge { - font-family: var(--font-mono); - font-size: 0.75rem; - padding: 3px 6px; - border-radius: 999px; - border: 1px solid var(--border-color); +.tasks-board-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + z-index: 10; + min-width: 320px; + max-width: 420px; + max-height: 60vh; + overflow: auto; background: var(--bg-tertiary); - color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35); + padding: 6px; } -.pr-badge.draft { - border-color: var(--accent-warning); +.tasks-board-menu-search-wrap { + position: sticky; + top: 0; + z-index: 1; + background: var(--bg-tertiary); + padding: 6px; + border-bottom: 1px solid var(--border-color); + margin: -6px -6px 6px; } -.pr-subtitle { - font-weight: 600; +.tasks-board-menu-search { + width: 100%; + background: var(--bg-secondary); color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 8px 10px; + font-size: 0.85rem; } -.pr-meta { - font-size: 0.75rem; - color: var(--text-tertiary); +.tasks-board-menu-search:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; } -.pr-actions { +.tasks-board-menu.hidden { + display: none; +} + +.tasks-board-menu-item { + width: 100%; display: flex; - gap: var(--space-sm); - flex-shrink: 0; + align-items: center; + gap: 10px; + background: transparent; + border: 1px solid transparent; + color: var(--text-primary); + border-radius: var(--radius-md); + padding: 8px 10px; + cursor: pointer; + text-align: left; } -/* ============================================ - Tasks Panel Styles - ============================================ */ +.tasks-board-menu-item:hover { + background: rgba(31, 111, 235, 0.12); + border-color: rgba(31, 111, 235, 0.25); +} -.queue-summary { - display: flex; - gap: 8px; - flex-wrap: wrap; - padding: 0 0 10px 0; +.tasks-board-menu-item.is-active { + border-color: rgba(31, 111, 235, 0.5); } -.tasks-modal .tasks-content { - /* Tasks is a primary workflow: prefer a robust, full-viewport panel. */ - max-width: none; - box-sizing: border-box; - width: 100%; - max-width: 100vw; - max-height: 100vh; - height: 100%; - display: flex; - flex-direction: column; +.tasks-board-menu-item.is-selected { + background: rgba(31, 111, 235, 0.18); + border-color: rgba(31, 111, 235, 0.35); +} + +.tasks-board-menu-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 999px; + border: 1px solid var(--border-color); + flex: 0 0 auto; +} + +.tasks-board-menu-dot.is-hidden { + visibility: hidden; +} + +.tasks-board-menu-label { overflow: hidden; - position: relative; - border-radius: 0; - padding: var(--space-md); - --tasks-board-accent: var(--accent-primary); - direction: ltr; - text-align: left; + text-overflow: ellipsis; + white-space: nowrap; } -.tasks-modal .tasks-content.tasks-has-board-accent .tasks-toolbar { - border-bottom-color: color-mix(in srgb, var(--border-color) 55%, var(--tasks-board-accent) 45%); +.tasks-board-menu-empty { + padding: 10px 12px; + color: var(--text-tertiary); + font-size: 0.85rem; } -.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header { - background: color-mix(in srgb, var(--bg-tertiary) 86%, var(--tasks-board-accent) 14%); +.tasks-hotkeys-overlay { + position: absolute; + inset: 0; + background: var(--overlay-backdrop); + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + z-index: 20; } -.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header:hover { - background: color-mix(in srgb, var(--bg-primary) 86%, var(--tasks-board-accent) 14%); +.tasks-launch-popover-overlay { + position: absolute; + inset: 0; + background: var(--overlay-backdrop-faint); + z-index: 19; +} + +.tasks-launch-popover { + position: absolute; + width: min(460px, 92vw); + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-float); + padding: 12px; } -.tasks-modal .tasks-body { - direction: ltr; - text-align: left; +.tasks-launch-popover-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; } -.tasks-modal { - background: transparent; +.tasks-launch-popover-title { + font-weight: 800; + color: var(--text-primary); } -.tasks-modal.tasks-theme-light { - --bg-primary: #ffffff; - --bg-secondary: #f6f8fa; - --bg-tertiary: #e6e8eb; - --text-primary: #24292f; - --text-secondary: #57606a; - --text-tertiary: #6e7781; - --border-color: #d0d7de; +.tasks-launch-popover-meta { + font-size: 0.8rem; + color: var(--text-tertiary); + margin-bottom: 10px; } -.tasks-modal .modal-header { - position: sticky; - top: 0; - z-index: 5; +.tasks-launch-popover-warn { + font-size: 0.85rem; + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 10px 12px; background: var(--bg-secondary); - padding-bottom: var(--space-sm); -} - -.tasks-modal .tasks-close-btn { - width: 44px; - height: 44px; - font-size: 1.6rem; - color: var(--accent-danger); - border: 1px solid rgba(248, 81, 73, 0.35); - background: rgba(248, 81, 73, 0.12); + margin-bottom: 10px; } -.tasks-modal .tasks-close-btn:hover { - background: rgba(248, 81, 73, 0.2); - border-color: rgba(248, 81, 73, 0.55); - color: var(--accent-danger); +.tasks-launch-popover-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + align-items: end; } -.tasks-modal .tasks-close-btn:focus-visible { - outline: 2px solid var(--accent-danger); - outline-offset: 2px; +.tasks-launch-popover-field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.8rem; + color: var(--text-secondary); } -.tasks-view-toggle { +.tasks-launch-popover-actions { display: flex; gap: 6px; align-items: center; @@ -9860,9 +10944,15 @@ header h1 { max-width: 100%; } -.tasks-view-btn.active { - border-color: var(--accent-primary); - box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15); +.tasks-hotkeys-card { + width: min(860px, 96vw); + max-height: min(80vh, 760px); + overflow: auto; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); + padding: 14px; } .tasks-view-btn { @@ -9873,50 +10963,58 @@ header h1 { position: relative; } -.tasks-filter summary { - list-style: none; +.tasks-hotkeys-title { + font-weight: 800; + font-size: 1rem; + color: var(--text-primary); } -.tasks-filter summary::-webkit-details-marker { - display: none; +.tasks-hotkeys-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; } -.tasks-filter-popover { - position: absolute; - top: calc(100% + 8px); - left: 0; - min-width: 240px; - max-height: 320px; - overflow: auto; - z-index: 5; +.tasks-hotkeys-group { border: 1px solid var(--border-color); - border-radius: var(--radius-md); background: var(--bg-secondary); - box-shadow: 0 10px 24px rgba(0,0,0,0.12); + border-radius: var(--radius-md); padding: 10px; } -.tasks-filter-actions { - display: flex; - gap: 8px; - margin-bottom: 10px; +.tasks-hotkeys-group-title { + font-weight: 800; + font-size: 0.85rem; + color: var(--text-primary); + margin-bottom: 8px; } -.tasks-filter-list { - display: flex; - flex-direction: column; - gap: 6px; +.tasks-hotkeys-row { + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.3; + margin-bottom: 6px; } -.tasks-filter-item { - display: flex; +.tasks-hotkeys-row code { + font-size: 0.8rem; + background: rgba(31, 111, 235, 0.12); + border: 1px solid rgba(31, 111, 235, 0.2); + border-radius: 6px; + padding: 2px 6px; + color: var(--text-primary); +} + +.tasks-toggle { + display: inline-flex; align-items: center; gap: 8px; - font-size: 0.85rem; - color: var(--text-primary); + font-size: 0.8rem; + color: var(--text-secondary); + user-select: none; } -.tasks-filter-item input { +.tasks-toggle input { accent-color: var(--accent-primary); } @@ -9927,7 +11025,11 @@ header h1 { display: flex; gap: var(--space-sm); align-items: center; - flex-wrap: wrap; + gap: 6px; + padding: 4px 6px; + border: 1px solid var(--border-color); + border-radius: 999px; + background: var(--bg-secondary); } #queue-panel .tasks-toolbar { @@ -9999,389 +11101,460 @@ header h1 { align-items: center; gap: 6px; padding: 4px 8px; - border: 1px solid var(--border-color); border-radius: 999px; - background: var(--bg-secondary); + font-size: 0.8rem; + color: var(--text-secondary); + cursor: pointer; + user-select: none; } -.tasks-launch-default-tier-group { - padding: 0; - border: none; - background: transparent; +.tasks-radio-option input { + margin: 0; + accent-color: var(--accent-primary); } -.tasks-launch-default-agent-group { - padding: 0; - border: none; - background: transparent; +.tasks-radio-option:has(input:checked) { + background: rgba(31, 111, 235, 0.15); + color: var(--text-primary); } -.tasks-launch-default-mode-group { - padding: 0; - border: none; - background: transparent; +.tasks-radio-option:has(input:focus-visible) { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; } -.tasks-launch-defaults-label { - font-weight: 900; +.tasks-select { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 6px 10px; font-size: 0.85rem; - color: var(--text-secondary); + min-width: 160px; } -.tasks-toggle.tasks-toggle-mini { - font-size: 0.75rem; - gap: 6px; +.tasks-search { + min-width: 220px; + flex: 1; } -.tasks-toggle.tasks-toggle-mini span { - opacity: 0.9; +.tasks-body { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: 420px 1fr; + grid-template-rows: 1fr; + grid-template-areas: "cards detail"; } -.tasks-board-accent { - width: 12px; - height: 12px; - border-radius: 999px; - background: var(--tasks-board-accent); - border: 1px solid var(--border-color); - box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.12); +.tasks-body.tasks-body-board { + /* Board view: show full-width kanban until a card is selected. */ + grid-template-columns: 1fr; + grid-template-areas: "cards"; + position: relative; + overflow: hidden; +} + +.tasks-body.tasks-body-board .tasks-detail { + display: none; +} + +.tasks-body.tasks-body-board .tasks-cards { + grid-column: 1; + border-left: none; +} + +.tasks-body.tasks-body-board.tasks-kanban-wrap .tasks-cards { + overflow: auto; +} + +.tasks-body.tasks-body-board.tasks-has-detail { + /* When a card is selected, show details as an overlay on the right. */ + grid-template-columns: 1fr; +} + +.tasks-body.tasks-body-board.tasks-has-detail .tasks-detail { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 520px; + max-width: min(520px, 65vw); + border-left: 1px solid var(--border-color); + box-shadow: -10px 0 24px rgba(0, 0, 0, 0.25); + z-index: 5; +} + +.tasks-body.tasks-body-board.tasks-has-detail .tasks-cards { + grid-column: 1; + border-left: none; +} + +.tasks-cards { + grid-area: cards; + grid-column: 1; + grid-row: 1; + overflow-y: auto; + padding: var(--space-md); + border-right: 1px solid var(--border-color); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + gap: var(--space-sm); + min-width: 0; } -.tasks-board-accent.is-hidden { - display: none; +.tasks-body.tasks-body-board .tasks-cards { + border-right: none; + overflow: hidden; + padding: var(--space-sm); + background: var(--bg-primary); } -.tasks-board-picker { - position: relative; - display: inline-flex; - align-items: center; - gap: 8px; +.tasks-board { + height: 100%; + display: flex; + gap: var(--space-md); + justify-content: flex-start; + overflow-x: auto; + overflow-y: hidden; + padding-bottom: var(--space-sm); + scroll-snap-type: x mandatory; } -.tasks-board-btn { - min-width: 200px; - text-align: left; - justify-content: flex-start; +.tasks-board.tasks-board-wrap { + flex-wrap: wrap; + overflow-x: hidden; + overflow-y: auto; + align-content: flex-start; + scroll-snap-type: none; } -.tasks-select-hidden { - position: absolute; - left: -9999px; - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; +.tasks-board.tasks-board-wrap.tasks-board-grid .tasks-column-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + align-content: start; } -.tasks-board-menu { - position: absolute; - top: calc(100% + 6px); - left: 0; - z-index: 10; - min-width: 320px; - max-width: 420px; - max-height: 60vh; - overflow: auto; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35); - padding: 6px; +.tasks-board.tasks-board-expand .tasks-column-cards { + display: grid; + grid-auto-flow: column; + grid-template-rows: repeat(var(--tasks-card-rows, 1), min-content); + grid-template-columns: repeat(var(--tasks-card-columns, 1), minmax(180px, 1fr)); + align-content: start; + gap: var(--space-sm); + overflow: hidden; + flex: 1; + min-height: 0; } -.tasks-board-menu-search-wrap { - position: sticky; - top: 0; - z-index: 1; - background: var(--bg-tertiary); - padding: 6px; - border-bottom: 1px solid var(--border-color); - margin: -6px -6px 6px; +.tasks-board.tasks-board-grid .task-card-board { + height: fit-content; } -.tasks-board-menu-search { - width: 100%; +.tasks-column { + --tasks-col-expanded: clamp(240px, 22vw, 360px); + --tasks-col-collapsed: 56px; + --tasks-card-columns: 1; + --tasks-card-rows: 1; + width: var(--tasks-col-expanded); + min-width: var(--tasks-col-expanded); background: var(--bg-secondary); - color: var(--text-primary); border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: 8px 10px; - font-size: 0.85rem; -} - -.tasks-board-menu-search:focus-visible { - outline: 2px solid var(--accent-primary); - outline-offset: 2px; + display: flex; + flex-direction: column; + max-height: 100%; + scroll-snap-align: start; + position: relative; } -.tasks-board-menu.hidden { - display: none; +.tasks-column.hover { + border-color: var(--tasks-board-accent); + box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15); } -.tasks-board-menu-item { - width: 100%; +.tasks-column-header { + padding: var(--space-sm) var(--space-md); display: flex; - align-items: center; - gap: 10px; - background: transparent; - border: 1px solid transparent; - color: var(--text-primary); - border-radius: var(--radius-md); - padding: 8px 10px; + align-items: baseline; + justify-content: space-between; + gap: var(--space-sm); + width: 100%; + background: var(--bg-tertiary); + border: none; + border-bottom: 1px solid var(--border-color); cursor: pointer; text-align: left; + color: var(--text-primary); } -.tasks-board-menu-item:hover { - background: rgba(31, 111, 235, 0.12); - border-color: rgba(31, 111, 235, 0.25); -} - -.tasks-board-menu-item.is-active { - border-color: rgba(31, 111, 235, 0.5); -} - -.tasks-board-menu-item.is-selected { - background: rgba(31, 111, 235, 0.18); - border-color: rgba(31, 111, 235, 0.35); -} - -.tasks-board-menu-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 999px; - border: 1px solid var(--border-color); - flex: 0 0 auto; +.tasks-column-header:hover { + background: var(--bg-primary); } -.tasks-board-menu-dot.is-hidden { - visibility: hidden; +.tasks-column-header:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; } -.tasks-board-menu-label { +.tasks-column-title { + font-weight: 700; + font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + color: var(--text-primary); } -.tasks-board-menu-empty { - padding: 10px 12px; +.tasks-column-count { + font-family: var(--font-mono); + font-size: 0.8rem; color: var(--text-tertiary); - font-size: 0.85rem; } -.tasks-hotkeys-overlay { - position: absolute; - inset: 0; - background: var(--overlay-backdrop); +.tasks-column-cards { + overflow-y: auto; + padding: var(--space-sm); display: flex; - align-items: center; - justify-content: center; - padding: 16px; - z-index: 20; -} - -.tasks-launch-popover-overlay { - position: absolute; - inset: 0; - background: var(--overlay-backdrop-faint); - z-index: 19; + flex-direction: column; + gap: var(--space-sm); } -.tasks-launch-popover { - position: absolute; - width: min(460px, 92vw); - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - box-shadow: var(--shadow-float); - padding: 12px; +.tasks-column.is-collapsed { + width: var(--tasks-col-collapsed); + min-width: var(--tasks-col-collapsed); } -.tasks-launch-popover-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 8px; +.tasks-column.is-collapsed .tasks-column-cards { + display: none; } -.tasks-launch-popover-title { +.tasks-column.is-collapsed .tasks-column-title { + writing-mode: vertical-rl; + transform: rotate(180deg); + white-space: nowrap; + overflow: visible; + text-overflow: unset; + font-size: 0.9rem; font-weight: 800; - color: var(--text-primary); + letter-spacing: 0.04em; + text-transform: uppercase; } -.tasks-launch-popover-meta { - font-size: 0.8rem; - color: var(--text-tertiary); - margin-bottom: 10px; +.tasks-column.is-collapsed .tasks-column-header { + justify-content: flex-start; + align-items: center; + flex-direction: column; + height: 100%; + padding: 10px 8px; + gap: 14px; } -.tasks-launch-popover-warn { - font-size: 0.85rem; - color: var(--text-secondary); +.tasks-column.is-collapsed .tasks-column-count { + display: inline-flex; + min-width: 30px; + height: 30px; + padding: 0 10px; + border-radius: 999px; border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: 10px 12px; background: var(--bg-secondary); - margin-bottom: 10px; + align-items: center; + justify-content: center; + font-weight: 700; + color: var(--text-primary); + order: -1; } -.tasks-launch-popover-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - align-items: end; +.task-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; } -.tasks-launch-popover-field { - display: flex; - flex-direction: column; - gap: 6px; - font-size: 0.8rem; - color: var(--text-secondary); +.task-card-top-right { + display: inline-flex; + align-items: center; + gap: 8px; + flex-shrink: 0; } -.tasks-launch-popover-actions { - display: flex; - gap: 10px; - justify-content: flex-end; - margin-top: 12px; +.task-card-quick-actions { + display: inline-flex; + align-items: center; + gap: 6px; } -.tasks-hotkeys-card { - width: min(860px, 96vw); - max-height: min(80vh, 760px); - overflow: auto; - background: var(--bg-primary); +.tasks-quick-tier-group { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px; border: 1px solid var(--border-color); - border-radius: var(--radius-md); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); - padding: 14px; + border-radius: 999px; + background: var(--bg-secondary); } -.tasks-hotkeys-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; +.tasks-quick-tier-btn { + padding: 3px 7px; + font-size: 0.72rem; + font-weight: 900; + line-height: 1; + border-radius: 999px; } -.tasks-hotkeys-title { - font-weight: 800; - font-size: 1rem; +.tasks-quick-tier-btn.is-selected { + border-color: var(--accent-primary); + background: rgba(31, 111, 235, 0.18); color: var(--text-primary); } -.tasks-hotkeys-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 12px; +.tasks-select.tasks-select-mini { + min-width: 64px; + padding: 4px 6px; + font-size: 0.75rem; + font-weight: 800; } -.tasks-hotkeys-group { +.tasks-quick-launch-btn { + padding: 4px 8px; + font-size: 0.8rem; + line-height: 1; +} + +.task-card-labels { + display: inline-flex; + gap: 6px; + flex-wrap: nowrap; + overflow: hidden; + min-width: 0; +} + +.tasks-label { + display: inline-flex; + align-items: center; + padding: 2px 6px; + border-radius: 999px; + font-size: 0.7rem; + line-height: 1.2; border: 1px solid var(--border-color); background: var(--bg-secondary); - border-radius: var(--radius-md); - padding: 10px; + color: var(--text-secondary); + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.tasks-hotkeys-group-title { - font-weight: 800; - font-size: 0.85rem; - color: var(--text-primary); - margin-bottom: 8px; +.tasks-label-editor { + display: flex; + flex-wrap: wrap; + gap: 6px; } -.tasks-hotkeys-row { - font-size: 0.85rem; - color: var(--text-secondary); - line-height: 1.3; - margin-bottom: 6px; +.tasks-label-toggle { + appearance: none; + cursor: pointer; } -.tasks-hotkeys-row code { - font-size: 0.8rem; - background: rgba(31, 111, 235, 0.12); - border: 1px solid rgba(31, 111, 235, 0.2); - border-radius: 6px; - padding: 2px 6px; - color: var(--text-primary); +.tasks-label-toggle.is-selected { + box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.25); } -.tasks-toggle { +.tasks-checkbox { display: inline-flex; align-items: center; gap: 8px; font-size: 0.8rem; color: var(--text-secondary); - user-select: none; } -.tasks-toggle input { +.tasks-checkbox input { + margin: 0; accent-color: var(--accent-primary); } -.tasks-radio { +.tasks-label--green { border-color: #2ea043; background: rgba(46, 160, 67, 0.12); color: var(--text-primary); } +.tasks-label--yellow { border-color: #d29922; background: rgba(210, 153, 34, 0.14); color: var(--text-primary); } +.tasks-label--orange { border-color: #f78166; background: rgba(247, 129, 102, 0.14); color: var(--text-primary); } +.tasks-label--red { border-color: #f85149; background: rgba(248, 81, 73, 0.14); color: var(--text-primary); } +.tasks-label--purple { border-color: #a371f7; background: rgba(163, 113, 247, 0.14); color: var(--text-primary); } +.tasks-label--blue { border-color: #1f6feb; background: rgba(31, 111, 235, 0.14); color: var(--text-primary); } +.tasks-label--sky { border-color: #79c0ff; background: rgba(121, 192, 255, 0.14); color: var(--text-primary); } +.tasks-label--lime { border-color: #7ee787; background: rgba(126, 231, 135, 0.14); color: var(--text-primary); } +.tasks-label--pink { border-color: #ff80c8; background: rgba(255, 128, 200, 0.14); color: var(--text-primary); } +.tasks-label--black { border-color: #30363d; background: rgba(48, 54, 61, 0.6); color: var(--text-primary); } +.tasks-label--more { border-color: var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); } + +.task-card-assignees { display: inline-flex; - align-items: center; gap: 6px; - padding: 4px 6px; - border: 1px solid var(--border-color); - border-radius: 999px; - background: var(--bg-secondary); + flex-shrink: 0; } -.tasks-radio-option { +.tasks-avatar { + width: 22px; + height: 22px; + border-radius: 999px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); display: inline-flex; align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: 999px; - font-size: 0.8rem; + justify-content: center; + overflow: hidden; color: var(--text-secondary); - cursor: pointer; - user-select: none; + font-size: 0.75rem; + text-decoration: none; } -.tasks-radio-option input { - margin: 0; - accent-color: var(--accent-primary); +.tasks-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; } -.tasks-radio-option:has(input:checked) { - background: rgba(31, 111, 235, 0.15); - color: var(--text-primary); +.tasks-avatar-more { + font-family: var(--font-mono); + font-size: 0.7rem; } -.tasks-radio-option:has(input:focus-visible) { - outline: 2px solid var(--accent-primary); - outline-offset: 2px; +.task-card-due { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--text-secondary); } -.tasks-select { - background: var(--bg-secondary); - color: var(--text-primary); +.tasks-kv { + display: flex; + flex-direction: column; + gap: 6px; +} + +.tasks-kv-row { + display: grid; + grid-template-columns: 140px 1fr; + gap: 10px; + padding: 8px 10px; border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: 6px 10px; - font-size: 0.85rem; - min-width: 160px; + background: var(--bg-secondary); } -.tasks-search { - min-width: 220px; - flex: 1; +.tasks-kv-row-edit { + align-items: center; } -.tasks-body { - flex: 1; - min-height: 0; - display: grid; - grid-template-columns: 420px 1fr; - grid-template-rows: 1fr; - grid-template-areas: "cards detail"; +.tasks-kv-key { + color: var(--text-secondary); + font-size: 0.8rem; + font-weight: 700; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } #queue-panel .tasks-body { @@ -10407,106 +11580,139 @@ header h1 { margin-top: var(--space-sm); } -.tasks-body.tasks-body-board { - /* Board view: show full-width kanban until a card is selected. */ - grid-template-columns: 1fr; - grid-template-areas: "cards"; - position: relative; - overflow: hidden; +.tasks-body.tasks-body-board { + /* Board view: show full-width kanban until a card is selected. */ + grid-template-columns: 1fr; + grid-template-areas: "cards"; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tasks-kv-val-edit { + overflow: visible; + text-overflow: unset; + white-space: normal; + display: flex; + align-items: center; + gap: 8px; +} + +.task-card-board { + cursor: grab; +} + +.task-card-board.dragging { + opacity: 0.6; + cursor: grabbing; +} + +.task-card-row { + padding: var(--space-sm) var(--space-md); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-tertiary); + cursor: pointer; + transition: border-color 0.15s, transform 0.15s; +} + +.task-card-list { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.task-card-list-main { + min-width: 0; + flex: 1; } -.tasks-body.tasks-body-board .tasks-detail { - display: none; +.task-card-list-actions { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 6px; } -.tasks-body.tasks-body-board .tasks-cards { - grid-column: 1; - border-left: none; +.task-card-row:hover { + border-color: var(--accent-primary); + transform: translateY(-1px); } -.tasks-body.tasks-body-board.tasks-kanban-wrap .tasks-cards { - overflow: auto; +.task-card-row.active { + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2); } -.tasks-body.tasks-body-board.tasks-has-detail { - /* When a card is selected, show details as an overlay on the right. */ - grid-template-columns: 1fr; +.task-card-title { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; } -.tasks-body.tasks-body-board.tasks-has-detail .tasks-detail { - display: block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 520px; - max-width: min(520px, 65vw); - border-left: 1px solid var(--border-color); - box-shadow: -10px 0 24px rgba(0, 0, 0, 0.25); - z-index: 5; +.tasks-card-board-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 999px; + border: 1px solid var(--border-color); + margin-right: 8px; + transform: translateY(1px); } -.tasks-body.tasks-body-board.tasks-has-detail .tasks-cards { - grid-column: 1; - border-left: none; +.task-card-meta { + font-size: 0.75rem; + color: var(--text-tertiary); } -.tasks-cards { - grid-area: cards; - grid-column: 1; +.tasks-detail { + grid-area: detail; + grid-column: 2; grid-row: 1; overflow-y: auto; - padding: var(--space-md); - border-right: 1px solid var(--border-color); - background: var(--bg-secondary); - display: flex; - flex-direction: column; - gap: var(--space-sm); + padding: var(--space-lg); + background: var(--bg-primary); min-width: 0; } -.tasks-body.tasks-body-board .tasks-cards { - border-right: none; - overflow: hidden; - padding: var(--space-sm); - background: var(--bg-primary); +.tasks-detail-empty { + color: var(--text-secondary); } -.tasks-board { - height: 100%; +.tasks-detail-header { display: flex; + align-items: flex-start; + justify-content: space-between; gap: var(--space-md); - justify-content: flex-start; - overflow-x: auto; - overflow-y: hidden; - padding-bottom: var(--space-sm); - scroll-snap-type: x mandatory; + margin-bottom: var(--space-sm); } -.tasks-board.tasks-board-wrap { - flex-wrap: wrap; - overflow-x: hidden; - overflow-y: auto; - align-content: flex-start; - scroll-snap-type: none; +.tasks-detail-title { + font-weight: 700; + font-size: 1rem; + line-height: 1.2; } -.tasks-board.tasks-board-wrap.tasks-board-grid .tasks-column-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - align-content: start; +.tasks-detail-actions { + display: flex; + gap: var(--space-sm); + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + flex-shrink: 0; } -.tasks-board.tasks-board-expand .tasks-column-cards { - display: grid; - grid-auto-flow: column; - grid-template-rows: repeat(var(--tasks-card-rows, 1), min-content); - grid-template-columns: repeat(var(--tasks-card-columns, 1), minmax(180px, 1fr)); - align-content: start; - gap: var(--space-sm); - overflow: hidden; - flex: 1; - min-height: 0; +.tasks-input { + width: 100%; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 8px 10px; + font-size: 0.95rem; + font-weight: 700; } .tasks-board.tasks-board-expand .task-card-title { @@ -10522,1143 +11728,1163 @@ header h1 { height: fit-content; } -.tasks-column { - --tasks-col-expanded: clamp(240px, 22vw, 360px); - --tasks-col-collapsed: 56px; - --tasks-card-columns: 1; - --tasks-card-rows: 1; - width: var(--tasks-col-expanded); - min-width: var(--tasks-col-expanded); +.tasks-textarea { + width: 100%; background: var(--bg-secondary); + color: var(--text-primary); border: 1px solid var(--border-color); border-radius: var(--radius-md); - display: flex; - flex-direction: column; - max-height: 100%; - scroll-snap-align: start; - position: relative; -} - -.tasks-column.hover { - border-color: var(--tasks-board-accent); - box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15); + padding: 8px 10px; + font-size: 0.85rem; + font-family: var(--font-mono); + resize: vertical; } -.tasks-column-header { - padding: var(--space-sm) var(--space-md); +.tasks-inline-row { display: flex; - align-items: baseline; - justify-content: space-between; gap: var(--space-sm); - width: 100%; - background: var(--bg-tertiary); - border: none; - border-bottom: 1px solid var(--border-color); - cursor: pointer; - text-align: left; - color: var(--text-primary); -} - -.tasks-column-header:hover { - background: var(--bg-primary); -} - -.tasks-column-header:focus-visible { - outline: 2px solid var(--accent-primary); - outline-offset: 2px; -} - -.tasks-column-title { - font-weight: 700; - font-size: 0.9rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--text-primary); -} - -.tasks-column-count { - font-family: var(--font-mono); - font-size: 0.8rem; - color: var(--text-tertiary); + align-items: center; + flex-wrap: wrap; } -.tasks-column-cards { - overflow-y: auto; - padding: var(--space-sm); +.tasks-combined-list { display: flex; flex-direction: column; - gap: var(--space-sm); -} - -.tasks-column.is-collapsed { - width: var(--tasks-col-collapsed); - min-width: var(--tasks-col-collapsed); -} - -.tasks-column.is-collapsed .tasks-column-cards { - display: none; -} - -.tasks-column.is-collapsed .tasks-column-title { - writing-mode: vertical-rl; - transform: rotate(180deg); - white-space: nowrap; - overflow: visible; - text-overflow: unset; - font-size: 0.9rem; - font-weight: 800; - letter-spacing: 0.04em; - text-transform: uppercase; + gap: 8px; } -.tasks-column.is-collapsed .tasks-column-header { - justify-content: flex-start; +.tasks-combined-item { + display: flex; align-items: center; - flex-direction: column; - height: 100%; - padding: 10px 8px; - gap: 14px; -} - -.tasks-column.is-collapsed .tasks-column-count { - display: inline-flex; - min-width: 30px; - height: 30px; - padding: 0 10px; - border-radius: 999px; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; border: 1px solid var(--border-color); + border-radius: var(--radius-md); background: var(--bg-secondary); - align-items: center; - justify-content: center; - font-weight: 700; - color: var(--text-primary); - order: -1; } -.task-card-top { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 6px; +.tasks-combined-label { + font-size: 0.85rem; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.task-card-top-right { +.tasks-combined-actions { display: inline-flex; - align-items: center; - gap: 8px; + gap: 6px; flex-shrink: 0; } -.task-card-quick-actions { +.tasks-chips { display: inline-flex; - align-items: center; gap: 6px; + flex-wrap: wrap; + margin-left: 6px; } -.tasks-quick-tier-group { +.tasks-chip { display: inline-flex; align-items: center; - gap: 4px; - padding: 2px; - border: 1px solid var(--border-color); + gap: 6px; + padding: 4px 8px; border-radius: 999px; + border: 1px solid var(--border-color); background: var(--bg-secondary); + font-size: 0.75rem; + color: var(--text-secondary); } -.tasks-quick-tier-btn { - padding: 3px 7px; - font-size: 0.72rem; - font-weight: 900; - line-height: 1; +.tasks-chip-avatar { + width: 16px; + height: 16px; border-radius: 999px; + object-fit: cover; + display: inline-block; } -.tasks-quick-tier-btn.is-selected { - border-color: var(--accent-primary); - background: rgba(31, 111, 235, 0.18); +.tasks-chip-link { + color: var(--text-secondary); + text-decoration: none; +} + +.tasks-chip-link:hover { color: var(--text-primary); + text-decoration: underline; } -.tasks-select.tasks-select-mini { - min-width: 64px; - padding: 4px 6px; - font-size: 0.75rem; - font-weight: 800; +.tasks-chip-muted { + opacity: 0.7; } -.tasks-quick-launch-btn { - padding: 4px 8px; - font-size: 0.8rem; +.tasks-chip-x { + appearance: none; + border: none; + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + font-size: 0.9rem; line-height: 1; + padding: 0 2px; } -.task-card-labels { - display: inline-flex; - gap: 6px; - flex-wrap: nowrap; - overflow: hidden; - min-width: 0; +.tasks-chip-x:hover { + color: var(--text-primary); } -.tasks-label { - display: inline-flex; +.tasks-detail-block { + margin-top: var(--space-md); +} + +.tasks-detail-block.tasks-dropzone-hover { + outline: 2px dashed var(--accent-color); + outline-offset: 6px; + border-radius: var(--radius-md); +} + +.tasks-detail-block-title { + font-size: 0.8rem; + color: var(--text-secondary); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 6px; +} + +.tasks-move-row { + display: flex; + gap: var(--space-sm); align-items: center; - padding: 2px 6px; - border-radius: 999px; - font-size: 0.7rem; - line-height: 1.2; +} + +.tasks-select-inline { + min-width: 240px; +} + +.tasks-comment-row { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.tasks-comments { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.tasks-comment { + padding: var(--space-sm) var(--space-md); border: 1px solid var(--border-color); + border-radius: var(--radius-md); background: var(--bg-secondary); - color: var(--text-secondary); - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -.tasks-label-editor { +.tasks-comment-meta { + font-size: 0.75rem; + color: var(--text-tertiary); + margin-bottom: 6px; +} + +.tasks-comment-text { + white-space: pre-wrap; + color: var(--text-primary); + line-height: 1.35; + font-size: 0.85rem; +} + +.tasks-deps { display: flex; - flex-wrap: wrap; + flex-direction: column; gap: 6px; } -.tasks-label-toggle { - appearance: none; - cursor: pointer; +.tasks-dep-row { + display: grid; + grid-template-columns: 18px 1fr auto; + gap: 10px; + align-items: center; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-secondary); } -.tasks-label-toggle.is-selected { - box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.25); +.tasks-dep-row.done { + opacity: 0.75; } -.tasks-checkbox { - display: inline-flex; - align-items: center; - gap: 8px; - font-size: 0.8rem; - color: var(--text-secondary); +.tasks-cover { + display: block; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-secondary); + overflow: hidden; } -.tasks-checkbox input { - margin: 0; - accent-color: var(--accent-primary); +.tasks-cover img { + display: block; + width: 100%; + max-height: 220px; + object-fit: cover; } -.tasks-label--green { border-color: #2ea043; background: rgba(46, 160, 67, 0.12); color: var(--text-primary); } -.tasks-label--yellow { border-color: #d29922; background: rgba(210, 153, 34, 0.14); color: var(--text-primary); } -.tasks-label--orange { border-color: #f78166; background: rgba(247, 129, 102, 0.14); color: var(--text-primary); } -.tasks-label--red { border-color: #f85149; background: rgba(248, 81, 73, 0.14); color: var(--text-primary); } -.tasks-label--purple { border-color: #a371f7; background: rgba(163, 113, 247, 0.14); color: var(--text-primary); } -.tasks-label--blue { border-color: #1f6feb; background: rgba(31, 111, 235, 0.14); color: var(--text-primary); } -.tasks-label--sky { border-color: #79c0ff; background: rgba(121, 192, 255, 0.14); color: var(--text-primary); } -.tasks-label--lime { border-color: #7ee787; background: rgba(126, 231, 135, 0.14); color: var(--text-primary); } -.tasks-label--pink { border-color: #ff80c8; background: rgba(255, 128, 200, 0.14); color: var(--text-primary); } -.tasks-label--black { border-color: #30363d; background: rgba(48, 54, 61, 0.6); color: var(--text-primary); } -.tasks-label--more { border-color: var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); } +.tasks-cover.tasks-cover-color { + height: 110px; +} -.task-card-assignees { - display: inline-flex; +.tasks-attachments { + display: flex; + flex-direction: column; gap: 6px; - flex-shrink: 0; } -.tasks-avatar { - width: 22px; - height: 22px; - border-radius: 999px; +.tasks-attachment { + display: flex; + gap: 10px; + align-items: center; + padding: 8px 10px; border: 1px solid var(--border-color); + border-radius: var(--radius-md); background: var(--bg-secondary); +} + +.tasks-attachment-thumb { display: inline-flex; + width: 44px; + height: 44px; + border-radius: var(--radius-sm); + overflow: hidden; + border: 1px solid var(--border-color); + background: rgba(0, 0, 0, 0.08); + flex: 0 0 auto; align-items: center; justify-content: center; - overflow: hidden; - color: var(--text-secondary); - font-size: 0.75rem; - text-decoration: none; } -.tasks-avatar img { +.tasks-attachment-thumb.is-empty { + opacity: 0.5; +} + +.tasks-attachment-thumb img { + display: block; width: 100%; height: 100%; object-fit: cover; - display: block; } -.tasks-avatar-more { - font-family: var(--font-mono); - font-size: 0.7rem; +.tasks-attachment-body { + min-width: 0; + flex: 1; } -.task-card-due { - font-family: var(--font-mono); - font-size: 0.72rem; - color: var(--text-secondary); +.tasks-attachment-name { + color: var(--text-primary); + font-size: 0.85rem; + line-height: 1.25; + text-decoration: none; + word-break: break-word; +} + +.tasks-attachment-name:hover { + text-decoration: underline; +} + +.tasks-attachment-meta { + margin-top: 2px; + color: var(--text-tertiary); + font-size: 0.75rem; + line-height: 1.2; + word-break: break-word; } -.tasks-kv { +.tasks-checklists { display: flex; flex-direction: column; - gap: 6px; + gap: var(--space-sm); } -.tasks-kv-row { - display: grid; - grid-template-columns: 140px 1fr; - gap: 10px; - padding: 8px 10px; +.tasks-checklist { border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-secondary); + padding: 10px; } -.tasks-kv-row-edit { +.tasks-checklist-header { + display: flex; + gap: 10px; align-items: center; + justify-content: space-between; } -.tasks-kv-key { - color: var(--text-secondary); - font-size: 0.8rem; +.tasks-checklist-title { font-weight: 700; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.tasks-kv-val { color: var(--text-primary); - font-size: 0.85rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + font-size: 0.9rem; + min-width: 0; + word-break: break-word; } -.tasks-kv-val-edit { - overflow: visible; - text-overflow: unset; - white-space: normal; +.tasks-checklist-actions { display: flex; + gap: 6px; align-items: center; - gap: 8px; } -.task-card-board { - cursor: grab; -} - -.task-card-board.dragging { - opacity: 0.6; - cursor: grabbing; +.tasks-checkitems { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 6px; } -.task-card-row { - padding: var(--space-sm) var(--space-md); +.tasks-checkitem-row { + display: grid; + grid-template-columns: 18px 1fr auto; + gap: 10px; + align-items: center; + padding: 8px 10px; border: 1px solid var(--border-color); border-radius: var(--radius-md); - background: var(--bg-tertiary); + background: var(--bg-primary); +} + +.tasks-checkitem-row.done { + opacity: 0.75; +} + +.tasks-checkitem-text { + min-width: 0; + color: var(--text-primary); + font-size: 0.85rem; cursor: pointer; - transition: border-color 0.15s, transform 0.15s; + word-break: break-word; } -.task-card-list { +.tasks-checkitem-add, +.tasks-checklist-add { + margin-top: 8px; +} + +.tasks-list-manager-row { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; gap: 10px; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-secondary); + margin-bottom: 8px; } -.task-card-list-main { +.tasks-list-manager-name { min-width: 0; flex: 1; + color: var(--text-primary); + font-weight: 600; + word-break: break-word; } -.task-card-list-actions { - flex-shrink: 0; - display: inline-flex; - align-items: center; +.tasks-list-manager-actions { + display: flex; gap: 6px; + align-items: center; } -.task-card-row:hover { - border-color: var(--accent-primary); - transform: translateY(-1px); +.tasks-dep-row.done .tasks-dep-text { + text-decoration: line-through; } -.task-card-row.active { - border-color: var(--accent-primary); - box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2); +.tasks-dep-text a { + color: var(--accent-primary); + text-decoration: none; } -.task-card-title { - font-weight: 600; - color: var(--text-primary); - margin-bottom: 4px; +.tasks-dep-text a:hover { + text-decoration: underline; } -.tasks-card-board-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 999px; - border: 1px solid var(--border-color); - margin-right: 8px; - transform: translateY(1px); +.tasks-dep-remove { + padding: 4px 8px; + font-weight: 700; } -.task-card-meta { - font-size: 0.75rem; +.tasks-dep-add { + margin-top: var(--space-sm); +} + +.tasks-detail-meta { + font-size: 0.8rem; color: var(--text-tertiary); + margin-bottom: var(--space-md); } -.tasks-detail { - grid-area: detail; - grid-column: 2; - grid-row: 1; - overflow-y: auto; - padding: var(--space-lg); - background: var(--bg-primary); - min-width: 0; +.tasks-detail-desc { + white-space: pre-wrap; + font-family: var(--font-mono); + font-size: 0.85rem; + padding: var(--space-md); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-secondary); + color: var(--text-primary); } -.tasks-detail-empty { - color: var(--text-secondary); +.tasks-config-hint { + padding: var(--space-lg); + border: 1px dashed var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-tertiary); } -.tasks-detail-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-md); +.tasks-config-title { + font-weight: 700; margin-bottom: var(--space-sm); } -.tasks-detail-title { - font-weight: 700; - font-size: 1rem; - line-height: 1.2; +.tasks-config-text { + color: var(--text-secondary); + line-height: 1.4; } -.tasks-detail-actions { - display: flex; - gap: var(--space-sm); - align-items: center; - flex-wrap: wrap; - justify-content: flex-end; - flex-shrink: 0; +/* Port panel - project detection styles */ +.port-name { + cursor: pointer; + transition: color 0.15s; } -.tasks-input { - width: 100%; - background: var(--bg-secondary); - color: var(--text-primary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: 8px 10px; - font-size: 0.95rem; - font-weight: 700; +.port-name:hover { + color: var(--accent-primary); } -.tasks-input-inline { - width: auto; - min-width: 220px; - display: inline-flex; - margin: 0 6px; - font-weight: 600; - font-size: 0.85rem; - padding: 6px 10px; +.port-name.custom-label { + color: #9f7aea; } -.tasks-textarea { - width: 100%; - background: var(--bg-secondary); - color: var(--text-primary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: 8px 10px; - font-size: 0.85rem; - font-family: var(--font-mono); - resize: vertical; +.port-path { + display: block; + font-size: 0.7rem; + color: var(--accent-primary); + opacity: 0.8; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.tasks-inline-row { +/* Port context - project/worktree info */ +.port-context { display: flex; - gap: var(--space-sm); align-items: center; - flex-wrap: wrap; + gap: 0.3rem; + font-size: 0.75rem; + margin-top: 2px; +} + +.port-project { + color: #9f7aea; + font-weight: 500; +} + +.port-worktree { + background: var(--accent-primary); + color: white; + padding: 1px 6px; + border-radius: 3px; + font-size: 0.7rem; + font-weight: 500; +} + +.port-subpath { + color: var(--text-muted); + font-size: 0.7rem; } -.tasks-combined-list { - display: flex; - flex-direction: column; - gap: 8px; +/* ============================================ + Sidebar Ports Section + ============================================ */ + +.sidebar-section { + border-top: 1px solid var(--border-color); + margin-top: auto; } -.tasks-combined-item { +.sidebar-section-header { display: flex; align-items: center; - justify-content: space-between; - gap: 10px; - padding: 10px 12px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + cursor: pointer; + user-select: none; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); + transition: background 0.15s; } -.tasks-combined-label { - font-size: 0.85rem; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.sidebar-section-header:hover { + background: var(--bg-tertiary); } -.tasks-combined-actions { - display: inline-flex; - gap: 6px; - flex-shrink: 0; +.ports-count { + background: var(--accent-primary); + color: white; + padding: 1px 6px; + border-radius: 10px; + font-size: 0.7rem; + font-weight: 600; + min-width: 18px; + text-align: center; } -.tasks-chips { - display: inline-flex; - gap: 6px; - flex-wrap: wrap; - margin-left: 6px; +.collapse-icon { + margin-left: auto; + font-size: 0.7rem; + transition: transform 0.2s; } -.tasks-chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--border-color); - background: var(--bg-secondary); - font-size: 0.75rem; - color: var(--text-secondary); +.sidebar-section.collapsed .collapse-icon { + transform: rotate(-90deg); } -.tasks-chip-avatar { - width: 16px; - height: 16px; - border-radius: 999px; - object-fit: cover; - display: inline-block; +.sidebar-section.collapsed .ports-sidebar-list { + display: none; } -.tasks-chip-link { - color: var(--text-secondary); - text-decoration: none; +.ports-sidebar-list { + max-height: 200px; + overflow-y: auto; + padding: var(--space-xs) 0; } -.tasks-chip-link:hover { - color: var(--text-primary); - text-decoration: underline; +.port-sidebar-item { + display: flex; + align-items: center; + gap: var(--space-xs); + padding: var(--space-xs) var(--space-md); + font-size: 0.75rem; + cursor: pointer; + transition: background 0.15s; + border-left: 2px solid transparent; } -.tasks-chip-muted { - opacity: 0.7; +.port-sidebar-item:hover { + background: var(--bg-tertiary); } -.tasks-chip-x { - appearance: none; - border: none; - background: transparent; - color: var(--text-tertiary); - cursor: pointer; +.port-sidebar-item.orchestrator { border-left-color: #9f7aea; } +.port-sidebar-item.node { border-left-color: #68d391; } +.port-sidebar-item.rails { border-left-color: #f56565; } +.port-sidebar-item.python { border-left-color: #ffd93d; } +.port-sidebar-item.vite { border-left-color: #48bb78; } + +.port-sidebar-icon { font-size: 0.9rem; - line-height: 1; - padding: 0 2px; + flex-shrink: 0; } -.tasks-chip-x:hover { +.port-sidebar-info { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.port-sidebar-name { + display: block; + font-weight: 500; color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.tasks-detail-block { - margin-top: var(--space-md); +.port-sidebar-context { + display: block; + font-size: 0.65rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.tasks-detail-block.tasks-dropzone-hover { - outline: 2px dashed var(--accent-color); - outline-offset: 6px; - border-radius: var(--radius-md); +.port-sidebar-port { + color: var(--accent-primary); + font-weight: 500; + font-size: 0.7rem; + flex-shrink: 0; } -.tasks-detail-block-title { - font-size: 0.8rem; - color: var(--text-secondary); - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.04em; - margin-bottom: 6px; +.ports-sidebar-empty { + padding: var(--space-sm) var(--space-md); + font-size: 0.75rem; + color: var(--text-muted); + font-style: italic; } -.tasks-move-row { - display: flex; - gap: var(--space-sm); - align-items: center; +/* ============================================ + Dashboard Ports Section + ============================================ */ + +.ports-dashboard-section { + margin-top: var(--space-lg); } -.tasks-select-inline { - min-width: 240px; +.ports-dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: var(--space-md); } -.tasks-comment-row { +.port-dashboard-card { display: flex; - flex-direction: column; + align-items: center; gap: var(--space-sm); + padding: var(--space-md); + background: var(--bg-secondary); + border-radius: var(--radius-md); + border-left: 3px solid var(--border-color); + cursor: pointer; + transition: all 0.15s; } -.tasks-comments { - display: flex; - flex-direction: column; - gap: var(--space-sm); +.port-dashboard-card:hover { + background: var(--bg-tertiary); + transform: translateY(-2px); + box-shadow: var(--shadow-soft); } -.tasks-comment { - padding: var(--space-sm) var(--space-md); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); +.port-dashboard-card.orchestrator { border-left-color: #9f7aea; } +.port-dashboard-card.node { border-left-color: #68d391; } +.port-dashboard-card.rails { border-left-color: #f56565; } +.port-dashboard-card.ruby { border-left-color: #f56565; } +.port-dashboard-card.python { border-left-color: #ffd93d; } +.port-dashboard-card.vite { border-left-color: #48bb78; } + +.port-card-icon { + font-size: 1.5rem; + flex-shrink: 0; } -.tasks-comment-meta { - font-size: 0.75rem; - color: var(--text-tertiary); - margin-bottom: 6px; +.port-card-info { + flex: 1; + min-width: 0; + overflow: hidden; } -.tasks-comment-text { - white-space: pre-wrap; +.port-card-name { + display: block; + font-weight: 600; color: var(--text-primary); - line-height: 1.35; - font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.tasks-deps { - display: flex; - flex-direction: column; - gap: 6px; +.port-card-context { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.tasks-dep-row { - display: grid; - grid-template-columns: 18px 1fr auto; - gap: 10px; - align-items: center; - padding: 8px 10px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); +.port-card-port { + font-size: 1rem; + font-weight: 700; + color: var(--accent-primary); + flex-shrink: 0; } -.tasks-dep-row.done { - opacity: 0.75; +.ports-empty, +.ports-loading { + grid-column: 1 / -1; + text-align: center; + padding: var(--space-lg); + color: var(--text-muted); + font-style: italic; } -.tasks-cover { - display: block; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); - overflow: hidden; -} +/* ============================================ + Dashboard Split Row Layout + ============================================ */ -.tasks-cover img { - display: block; - width: 100%; - max-height: 220px; - object-fit: cover; +.dashboard-split-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-lg); + margin-top: var(--space-lg); } -.tasks-cover.tasks-cover-color { - height: 110px; +.dashboard-half { + background: var(--bg-secondary); + border-radius: var(--radius-lg); + padding: var(--space-md); + max-height: 400px; + overflow-y: auto; } -.tasks-attachments { - display: flex; - flex-direction: column; - gap: 6px; +.dashboard-half h2 { + margin: 0 0 var(--space-sm) 0; + font-size: 1rem; + position: sticky; + top: 0; + background: var(--bg-secondary); + padding-bottom: var(--space-xs); } -.tasks-attachment { - display: flex; - gap: 10px; - align-items: center; - padding: 8px 10px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); +/* Grid layout for ports in dashboard half */ +.dashboard-half .ports-dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: var(--space-xs); } -.tasks-attachment-thumb { - display: inline-flex; - width: 44px; - height: 44px; - border-radius: var(--radius-sm); - overflow: hidden; - border: 1px solid var(--border-color); - background: rgba(0, 0, 0, 0.08); - flex: 0 0 auto; +.dashboard-half .port-dashboard-card { + padding: var(--space-sm); + flex-direction: column; align-items: center; - justify-content: center; + text-align: center; + gap: var(--space-xs); + min-height: 70px; } -.tasks-attachment-thumb.is-empty { - opacity: 0.5; +.dashboard-half .port-card-icon { + font-size: 1.3rem; } -.tasks-attachment-thumb img { - display: block; +.dashboard-half .port-card-info { width: 100%; - height: 100%; - object-fit: cover; } -.tasks-attachment-body { - min-width: 0; - flex: 1; +.dashboard-half .port-card-name { + font-size: 0.75rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.tasks-attachment-name { - color: var(--text-primary); - font-size: 0.85rem; - line-height: 1.25; - text-decoration: none; - word-break: break-word; +.dashboard-half .port-card-context { + font-size: 0.65rem; } -.tasks-attachment-name:hover { - text-decoration: underline; +.dashboard-half .port-card-port { + font-size: 0.8rem; + margin-top: auto; } -.tasks-attachment-meta { - margin-top: 2px; - color: var(--text-tertiary); - font-size: 0.75rem; - line-height: 1.2; - word-break: break-word; +/* Grid layout for quick links in dashboard half */ +.dashboard-half .quick-links-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: var(--space-xs); } -.tasks-checklists { - display: flex; +.dashboard-half .quick-link-item { flex-direction: column; - gap: var(--space-sm); + align-items: center; + text-align: center; + padding: var(--space-sm); + min-height: 60px; + gap: var(--space-xs); + border-left: none; + border-bottom: 2px solid var(--accent-primary); } -.tasks-checklist { - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); - padding: 10px; +.dashboard-half .quick-link-item .quick-link-icon { + font-size: 1.2rem; } -.tasks-checklist-header { - display: flex; - gap: 10px; - align-items: center; - justify-content: space-between; +.dashboard-half .quick-link-item .quick-link-label { + font-size: 0.7rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } -.tasks-checklist-title { - font-weight: 700; - color: var(--text-primary); - font-size: 0.9rem; - min-width: 0; - word-break: break-word; +/* Quick links container inside dashboard */ +.dashboard-half .quick-links-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: var(--space-xs); } -.tasks-checklist-actions { - display: flex; - gap: 6px; - align-items: center; +.dashboard-half .quick-links-section { + display: contents; } -.tasks-checkitems { - margin-top: 8px; - display: flex; - flex-direction: column; - gap: 6px; +.dashboard-half .quick-links-section h3 { + display: none; } -.tasks-checkitem-row { - display: grid; - grid-template-columns: 18px 1fr auto; - gap: 10px; - align-items: center; - padding: 8px 10px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-primary); +@media (max-width: 900px) { + .dashboard-split-row { + grid-template-columns: 1fr; + } } -.tasks-checkitem-row.done { - opacity: 0.75; -} +/* ============================================ + Quick Link Items (for dashboard half) + ============================================ */ -.tasks-checkitem-text { - min-width: 0; - color: var(--text-primary); - font-size: 0.85rem; +.quick-link-item { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + border-left: 3px solid var(--accent-primary); cursor: pointer; - word-break: break-word; + transition: all 0.15s; + text-decoration: none; + color: inherit; + border: none; + width: 100%; + text-align: left; + font-size: inherit; + font-family: inherit; } -.tasks-checkitem-add, -.tasks-checklist-add { - margin-top: 8px; +.quick-link-item:hover { + background: var(--bg-primary); + transform: translateX(2px); } -.tasks-list-manager-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 10px 12px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); - margin-bottom: 8px; +.quick-link-icon { + font-size: 1rem; + flex-shrink: 0; } -.tasks-list-manager-name { - min-width: 0; +.quick-link-label { flex: 1; + font-weight: 500; color: var(--text-primary); - font-weight: 600; - word-break: break-word; } -.tasks-list-manager-actions { - display: flex; - gap: 6px; - align-items: center; -} - -.tasks-dep-row.done .tasks-dep-text { - text-decoration: line-through; +.quick-links-empty { + padding: var(--space-md); + text-align: center; + color: var(--text-muted); + font-style: italic; + grid-column: 1 / -1; } -.tasks-dep-text a { - color: var(--accent-primary); - text-decoration: none; -} +/* ============================================ + Session Recovery Dialog + ============================================ */ -.tasks-dep-text a:hover { - text-decoration: underline; +.recovery-modal .modal-content { + max-width: 600px; + width: 90vw; } -.tasks-dep-remove { - padding: 4px 8px; - font-weight: 700; +.recovery-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-md) var(--space-lg); + border-bottom: 1px solid var(--border-color); + background: var(--bg-tertiary); } -.tasks-dep-add { - margin-top: var(--space-sm); +.recovery-header h2 { + margin: 0; + font-size: 1.1rem; + display: flex; + align-items: center; + gap: var(--space-sm); } -.tasks-detail-meta { +.recovery-info { + padding: var(--space-sm) var(--space-lg); + background: var(--bg-secondary); font-size: 0.8rem; - color: var(--text-tertiary); - margin-bottom: var(--space-md); + color: var(--text-muted); + border-bottom: 1px solid var(--border-color); } -.tasks-detail-desc { - white-space: pre-wrap; - font-family: var(--font-mono); - font-size: 0.85rem; +.recovery-sessions { + max-height: 300px; + overflow-y: auto; padding: var(--space-md); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); - color: var(--text-primary); } -.tasks-config-hint { - padding: var(--space-lg); - border: 1px dashed var(--border-color); - border-radius: var(--radius-md); +.recovery-session { + display: flex; + align-items: flex-start; + gap: var(--space-md); + padding: var(--space-sm) var(--space-md); + margin-bottom: var(--space-xs); background: var(--bg-tertiary); + border-radius: var(--radius-sm); + border-left: 3px solid var(--accent-primary); } -.tasks-config-title { - font-weight: 700; - margin-bottom: var(--space-sm); -} - -.tasks-config-text { - color: var(--text-secondary); - line-height: 1.4; -} - -/* Port panel - project detection styles */ -.port-name { - cursor: pointer; - transition: color 0.15s; +.recovery-session.selected { + border-left-color: #48bb78; + background: rgba(72, 187, 120, 0.1); } -.port-name:hover { - color: var(--accent-primary); +.recovery-checkbox { + margin-top: 2px; } -.port-name.custom-label { - color: #9f7aea; +.recovery-session-info { + flex: 1; + min-width: 0; } -.port-path { - display: block; - font-size: 0.7rem; - color: var(--accent-primary); - opacity: 0.8; - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.recovery-session-id { + font-weight: 600; + font-size: 0.9rem; + color: var(--text-primary); } -/* Port context - project/worktree info */ -.port-context { - display: flex; - align-items: center; - gap: 0.3rem; +.recovery-session-details { font-size: 0.75rem; + color: var(--text-muted); margin-top: 2px; } -.port-project { - color: #9f7aea; - font-weight: 500; +.recovery-session-details span { + display: inline-block; + margin-right: var(--space-sm); } -.port-worktree { - background: var(--accent-primary); - color: white; - padding: 1px 6px; - border-radius: 3px; - font-size: 0.7rem; - font-weight: 500; +.recovery-session-cwd { + color: var(--accent-primary); } -.port-subpath { - color: var(--text-muted); - font-size: 0.7rem; +.recovery-session-agent { + background: var(--bg-primary); + padding: 1px 6px; + border-radius: 3px; } -/* ============================================ - Sidebar Ports Section - ============================================ */ - -.sidebar-section { +.recovery-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-md) var(--space-lg); border-top: 1px solid var(--border-color); - margin-top: auto; + gap: var(--space-md); } -.sidebar-section-header { +.recovery-actions { display: flex; - align-items: center; gap: var(--space-sm); +} + +.recovery-skip { + color: var(--text-muted); + font-size: 0.8rem; +} + +.btn-recovery { padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + border: none; cursor: pointer; - user-select: none; - font-size: 0.85rem; font-weight: 500; - color: var(--text-secondary); - transition: background 0.15s; + transition: all 0.15s; } -.sidebar-section-header:hover { - background: var(--bg-tertiary); +.btn-recovery-all { + background: #48bb78; + color: white; } -.ports-count { +.btn-recovery-all:hover { + background: #38a169; +} + +.btn-recovery-selected { background: var(--accent-primary); - color: white; - padding: 1px 6px; - border-radius: 10px; - font-size: 0.7rem; - font-weight: 600; - min-width: 18px; - text-align: center; + color: var(--text-on-accent); } -.collapse-icon { - margin-left: auto; - font-size: 0.7rem; - transition: transform 0.2s; +.btn-recovery-selected:hover { + background: var(--accent-primary-hover); } -.sidebar-section.collapsed .collapse-icon { - transform: rotate(-90deg); +.btn-recovery-clear { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); } -.sidebar-section.collapsed .ports-sidebar-list { - display: none; +.btn-recovery-clear:hover { + border-color: var(--accent-danger); + background: rgba(248, 81, 73, 0.14); } -.ports-sidebar-list { - max-height: 200px; - overflow-y: auto; - padding: var(--space-xs) 0; +.btn-recovery-skip { + background: var(--bg-tertiary); + color: var(--text-secondary); } -.port-sidebar-item { - display: flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-xs) var(--space-md); - font-size: 0.75rem; - cursor: pointer; - transition: background 0.15s; - border-left: 2px solid transparent; +.btn-recovery-skip:hover { + background: var(--bg-primary); } -.port-sidebar-item:hover { - background: var(--bg-tertiary); +.no-recovery { + text-align: center; + padding: var(--space-lg); + color: var(--text-muted); } -.port-sidebar-item.orchestrator { border-left-color: #9f7aea; } -.port-sidebar-item.node { border-left-color: #68d391; } -.port-sidebar-item.rails { border-left-color: #f56565; } -.port-sidebar-item.python { border-left-color: #ffd93d; } -.port-sidebar-item.vite { border-left-color: #48bb78; } +/* ============================================ + Activity Feed Styles + ============================================ */ -.port-sidebar-icon { - font-size: 0.9rem; - flex-shrink: 0; +.activity-feed-modal .activity-feed-content { + max-width: 1200px; + width: 94vw; + max-height: 90vh; + display: flex; + flex-direction: column; } -.port-sidebar-info { +.activity-list { flex: 1; - min-width: 0; - overflow: hidden; + overflow-y: auto; + padding: var(--space-md) var(--space-lg); } -.port-sidebar-name { - display: block; - font-weight: 500; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.activity-empty { + padding: var(--space-lg); + color: var(--text-muted); } -.port-sidebar-context { - display: block; - font-size: 0.65rem; - color: var(--text-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.activity-event { + border: 1px solid var(--border-color); + background: var(--bg-secondary); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-bottom: var(--space-sm); +} + +.activity-event.activity-failed { + border-color: rgba(248, 81, 73, 0.55); + box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.18); +} + +.activity-meta { + display: flex; + gap: var(--space-sm); + align-items: center; + justify-content: space-between; } -.port-sidebar-port { - color: var(--accent-primary); - font-weight: 500; - font-size: 0.7rem; - flex-shrink: 0; +.activity-actions { + display: flex; + gap: var(--space-xs); + align-items: center; + margin-left: auto; } -.ports-sidebar-empty { - padding: var(--space-sm) var(--space-md); +.activity-action-btn { + padding: 4px 8px; font-size: 0.75rem; - color: var(--text-muted); - font-style: italic; + line-height: 1; } -/* ============================================ - Dashboard Ports Section - ============================================ */ - -.ports-dashboard-section { - margin-top: var(--space-lg); +.activity-time { + color: var(--text-muted); + font-size: 0.8rem; } -.ports-dashboard-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: var(--space-md); +.activity-kind { + font-family: var(--font-mono); + font-size: 0.8rem; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid transparent; + background: var(--bg-tertiary); + color: var(--text-secondary); } -.port-dashboard-card { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-md); - background: var(--bg-secondary); - border-radius: var(--radius-md); - border-left: 3px solid var(--border-color); - cursor: pointer; - transition: all 0.15s; +.activity-kind-agent { + border-color: var(--accent-primary); + color: var(--accent-primary); + background: rgba(31, 111, 235, 0.12); } -.port-dashboard-card:hover { - background: var(--bg-tertiary); - transform: translateY(-2px); - box-shadow: var(--shadow-soft); +.activity-kind-session { + border-color: var(--accent-success); + color: var(--accent-success); + background: rgba(63, 185, 80, 0.12); } -.port-dashboard-card.orchestrator { border-left-color: #9f7aea; } -.port-dashboard-card.node { border-left-color: #68d391; } -.port-dashboard-card.rails { border-left-color: #f56565; } -.port-dashboard-card.ruby { border-left-color: #f56565; } -.port-dashboard-card.python { border-left-color: #ffd93d; } -.port-dashboard-card.vite { border-left-color: #48bb78; } +.activity-kind-server { + border-color: var(--accent-warning); + color: var(--accent-warning); + background: rgba(210, 153, 34, 0.12); +} -.port-card-icon { - font-size: 1.5rem; - flex-shrink: 0; +.activity-kind-git { + border-color: var(--accent-success); + color: var(--accent-success); + background: rgba(63, 185, 80, 0.12); } -.port-card-info { - flex: 1; - min-width: 0; - overflow: hidden; +.activity-kind-pr { + border-color: var(--accent-primary); + color: var(--accent-primary); + background: rgba(31, 111, 235, 0.12); } -.port-card-name { - display: block; - font-weight: 600; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.activity-kind-tests { + border-color: var(--accent-warning); + color: var(--accent-warning); + background: rgba(210, 153, 34, 0.12); } -.port-card-context { - display: block; - font-size: 0.75rem; - color: var(--text-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.activity-kind-build { + border-color: var(--accent-warning); + color: var(--accent-warning); + background: rgba(210, 153, 34, 0.12); } -.port-card-port { - font-size: 1rem; - font-weight: 700; - color: var(--accent-primary); - flex-shrink: 0; +.activity-summary { + margin-top: var(--space-sm); + color: var(--text-primary); + font-size: 0.95rem; } -.ports-empty, -.ports-loading { - grid-column: 1 / -1; - text-align: center; - padding: var(--space-lg); +.activity-data { + margin-top: var(--space-sm); color: var(--text-muted); - font-style: italic; + font-family: var(--font-mono); + font-size: 0.8rem; + white-space: pre-wrap; + word-break: break-word; } -/* ============================================ - Dashboard Split Row Layout - ============================================ */ +/* ========================================================== + ONBOARDING OVERLAY (PERF-OPTIMIZED) + ========================================================== */ -.dashboard-split-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-lg); - margin-top: var(--space-lg); +.onboarding-overlay { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: + radial-gradient(90% 60% at 8% 12%, rgba(31, 111, 235, 0.22), transparent 75%), + radial-gradient(78% 55% at 92% 88%, rgba(0, 195, 255, 0.14), transparent 80%), + rgba(5, 8, 15, 0.86); + overflow: auto; + transition: opacity 0.18s ease-out, visibility 0.18s linear; } .dashboard-half { @@ -11669,13 +12895,18 @@ header h1 { overflow-y: auto; } -.dashboard-half h2 { - margin: 0 0 var(--space-sm) 0; - font-size: 1rem; - position: sticky; - top: 0; - background: var(--bg-secondary); - padding-bottom: var(--space-xs); +.onboarding-bg-glow { + position: absolute; + width: 52vw; + height: 52vw; + max-width: 640px; + max-height: 640px; + border-radius: 50%; + top: -16%; + left: -6%; + background: radial-gradient(circle, rgba(31, 111, 235, 0.2) 0%, rgba(31, 111, 235, 0) 72%); + opacity: 0.75; + pointer-events: none; } /* Grid layout for ports in dashboard half */ @@ -11698,8 +12929,9 @@ header h1 { font-size: 1.1rem; } -.dashboard-half .port-card-info { - width: 100%; +.onboarding-header { + margin-bottom: 4px; + text-align: center; } .dashboard-half .port-card-name { @@ -11725,9 +12957,14 @@ header h1 { gap: var(--space-xs); } -.dashboard-half .quick-link-item { - flex-direction: column; - align-items: center; +.onboarding-welcome-card { + padding: 28px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: + linear-gradient(155deg, rgba(56, 139, 253, 0.13), rgba(20, 29, 44, 0.42)), + rgba(255, 255, 255, 0.04); + box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28); text-align: center; padding: 6px; min-height: 52px; @@ -11755,99 +12992,100 @@ header h1 { gap: var(--space-xs); } -.dashboard-half .quick-links-section { - display: contents; +.onboarding-welcome-notes { + margin-top: 14px; + display: grid; + gap: 6px; + text-align: center; } -.dashboard-half .quick-links-section h3 { - display: none; +.onboarding-welcome-notes p { + margin: 0; + color: rgba(255, 255, 255, 0.74); + font-size: 0.9rem; + line-height: 1.45; } -@media (max-width: 900px) { - .dashboard-split-row { - grid-template-columns: 1fr; - } +.onboarding-welcome-actions { + margin-top: 46px; + justify-content: center; } -/* ============================================ - Quick Link Items (for dashboard half) - ============================================ */ - -.quick-link-item { +.onboarding-stepper-row { display: flex; align-items: center; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-md); - background: var(--bg-tertiary); - border-radius: var(--radius-sm); - border-left: 3px solid var(--accent-primary); - cursor: pointer; - transition: all 0.15s; - text-decoration: none; - color: inherit; - border: none; + justify-content: center; width: 100%; - text-align: left; - font-size: inherit; - font-family: inherit; + gap: 14px; + margin-bottom: 18px; + flex-wrap: wrap; + row-gap: 12px; } -.quick-link-item:hover { - background: var(--bg-primary); - transform: translateX(2px); +.onboarding-stepper-item { + display: flex; + align-items: center; + justify-content: center; } -.quick-link-icon { - font-size: 1rem; - flex-shrink: 0; +.stepper-icon-box { + display: flex; + align-items: center; + position: relative; } -.quick-link-label { - flex: 1; - font-weight: 500; - color: var(--text-primary); +.stepper-diamond { + width: 14px; + height: 14px; + border-radius: 3px; + transform: rotate(45deg); + transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; } -.quick-links-empty { - padding: var(--space-md); - text-align: center; - color: var(--text-muted); - font-style: italic; - grid-column: 1 / -1; +.stepper-upcoming .stepper-diamond { + background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); + box-shadow: inset 0 0 0 1px rgba(229, 231, 235, 0.26); + opacity: 0.95; } -/* ============================================ - Session Recovery Dialog - ============================================ */ - -.recovery-modal .modal-content { - max-width: 600px; - width: 90vw; +.stepper-active .stepper-icon-box { + gap: 0; } -.recovery-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--space-md) var(--space-lg); - border-bottom: 1px solid var(--border-color); - background: var(--bg-tertiary); +.stepper-active .stepper-diamond { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + box-shadow: 0 0 10px rgba(59, 130, 246, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.34); + transform: rotate(45deg) scale(1.15); + opacity: 1; } -.recovery-header h2 { - margin: 0; - font-size: 1.1rem; - display: flex; - align-items: center; - gap: var(--space-sm); +.stepper-done .stepper-diamond { + background: linear-gradient(135deg, #34d399 0%, #16a34a 100%); + box-shadow: 0 0 10px rgba(34, 197, 94, 0.38), inset 0 0 0 1px rgba(236, 253, 245, 0.34); + opacity: 1; } -.recovery-info { - padding: var(--space-sm) var(--space-lg); - background: var(--bg-secondary); - font-size: 0.8rem; - color: var(--text-muted); - border-bottom: 1px solid var(--border-color); +.stepper-active-label { + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + font-size: 0.88rem; + color: rgba(255, 255, 255, 0.9); + font-weight: 520; + margin-left: 0; + white-space: nowrap; +} + +.onboarding-step-card { + position: relative; + overflow: hidden; + padding: 26px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.045); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28); + transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; } .recovery-metrics { @@ -11893,254 +13131,318 @@ header h1 { padding: var(--space-md); } -.recovery-session { +.onboarding-step-icon { + width: 42px; + height: 42px; + margin-bottom: 18px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.11); + color: rgba(255, 255, 255, 0.94); display: flex; - align-items: flex-start; - gap: var(--space-md); - padding: var(--space-sm) var(--space-md); - margin-bottom: var(--space-xs); - background: var(--bg-tertiary); - border-radius: var(--radius-sm); - border-left: 3px solid var(--accent-primary); + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12); } -.recovery-session.selected { - border-left-color: #48bb78; - background: rgba(72, 187, 120, 0.1); +.onboarding-step-icon-svg { + width: 22px; + height: 22px; + display: block; } -.recovery-checkbox { - margin-top: 2px; +.onboarding-step-icon-svg * { + vector-effect: non-scaling-stroke; } -.recovery-session-info { - flex: 1; - min-width: 0; +.onboarding-step-title { + font-size: clamp(1.42rem, 2.9vw, 1.78rem); + font-weight: 620; + color: #ffffff; + margin-bottom: 10px; } -.recovery-session-id { - font-weight: 600; - font-size: 0.9rem; - color: var(--text-primary); +.onboarding-step-status-row { + display: flex; + align-items: flex-start; + gap: 10px; + margin-bottom: 20px; } -.recovery-session-details { - font-size: 0.75rem; - color: var(--text-muted); - margin-top: 2px; +.onboarding-check { + margin-top: 3px; + flex-shrink: 0; + color: #3fb950; } -.recovery-session-details span { - display: inline-block; - margin-right: var(--space-sm); +.onboarding-step-desc { + margin: 0; + font-size: 1rem; + line-height: 1.48; + color: rgba(255, 255, 255, 0.78); } -.recovery-session-cwd { - color: var(--accent-primary); +.onboarding-inline-status { + display: inline-block; + margin-left: 8px; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.9em; + background: rgba(0, 0, 0, 0.3); } -.recovery-session-agent { - background: var(--bg-primary); - padding: 1px 6px; - border-radius: 3px; -} +.onboarding-inline-status.status-ok { color: #3fb950; } +.onboarding-inline-status.status-missing { color: #f85149; } +.onboarding-inline-status.status-pending { color: #58a6ff; } -.recovery-footer { +.onboarding-step-actions { display: flex; - justify-content: space-between; - align-items: center; - padding: var(--space-md) var(--space-lg); - border-top: 1px solid var(--border-color); - gap: var(--space-md); + gap: 10px; + flex-wrap: wrap; } -.recovery-actions { - display: flex; - gap: var(--space-sm); +.onboarding-btn-secondary, +.onboarding-btn-back, +.onboarding-btn-primary { + border-radius: 10px; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; } -.recovery-skip { - color: var(--text-muted); - font-size: 0.8rem; +.onboarding-btn-secondary { + padding: 10px 18px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.09); + color: rgba(255, 255, 255, 0.92); + font-size: 0.94rem; + font-weight: 520; } -.btn-recovery { - padding: var(--space-sm) var(--space-md); - border-radius: var(--radius-sm); - border: none; - cursor: pointer; - font-weight: 500; - transition: all 0.15s; +.onboarding-btn-secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.16); + border-color: rgba(255, 255, 255, 0.26); + transform: translateY(-1px); } -.btn-recovery-all { - background: #48bb78; - color: white; +.onboarding-btn-secondary:disabled { + opacity: 0.52; + cursor: not-allowed; } -.btn-recovery-all:hover { - background: #38a169; +.onboarding-step-actions [data-setup-run] { + border-color: rgba(74, 222, 128, 0.55); + background: linear-gradient(135deg, #34d399 0%, #16a34a 100%); + color: #052e16; + box-shadow: 0 6px 16px rgba(22, 163, 74, 0.3); } -.btn-recovery-selected { - background: var(--accent-primary); - color: var(--text-on-accent); +.onboarding-step-actions [data-setup-run]:hover:not(:disabled) { + border-color: rgba(134, 239, 172, 0.92); + background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%); + color: #052e16; + box-shadow: 0 8px 18px rgba(22, 163, 74, 0.38); } -.btn-recovery-selected:hover { - background: var(--accent-primary-hover); +.onboarding-step-actions [data-setup-run]:disabled { + border-color: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.55); + box-shadow: none; } -.btn-recovery-clear { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border-color); +.onboarding-nav-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 6px; } -.btn-recovery-clear:hover { - border-color: var(--accent-danger); - background: rgba(248, 81, 73, 0.14); +.onboarding-nav-row.onboarding-welcome-actions { + justify-content: center; } -.btn-recovery-skip { - background: var(--bg-tertiary); - color: var(--text-secondary); +.onboarding-btn-back { + padding: 13px 24px; + border: 1px solid rgba(255, 255, 255, 0.11); + background: rgba(255, 255, 255, 0.055); + color: rgba(255, 255, 255, 0.74); + font-size: 0.98rem; + font-weight: 600; } -.btn-recovery-skip:hover { - background: var(--bg-primary); +.onboarding-btn-back:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.9); } -.no-recovery { - text-align: center; - padding: var(--space-lg); - color: var(--text-muted); +.onboarding-btn-primary { + padding: 13px 28px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: linear-gradient(135deg, #388bfd 0%, #1f6feb 100%); + color: #ffffff; + font-size: 0.98rem; + font-weight: 620; + box-shadow: 0 4px 13px rgba(31, 111, 235, 0.34); } -/* ============================================ - Activity Feed Styles - ============================================ */ - -.activity-feed-modal .activity-feed-content { - max-width: 1200px; - width: 94vw; - max-height: 90vh; - display: flex; - flex-direction: column; +.onboarding-btn-primary:hover:not(:disabled) { + background: linear-gradient(135deg, #58a6ff 0%, #388bfd 100%); + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(31, 111, 235, 0.44); } -.activity-list { - flex: 1; - overflow-y: auto; - padding: var(--space-md) var(--space-lg); +.onboarding-btn-primary:disabled { + border-color: transparent; + background: rgba(255, 255, 255, 0.11); + color: rgba(255, 255, 255, 0.45); + box-shadow: none; + cursor: not-allowed; } -.activity-empty { - padding: var(--space-lg); - color: var(--text-muted); +.dependency-git-identity-fields { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 18px; } -.activity-event { - border: 1px solid var(--border-color); - background: var(--bg-secondary); - border-radius: var(--radius-md); - padding: var(--space-md); - margin-bottom: var(--space-sm); +.dependency-git-identity-field { + display: flex; + flex-direction: column; + gap: 4px; } -.activity-event.activity-failed { - border-color: rgba(248, 81, 73, 0.55); - box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.18); +.dependency-git-identity-field span { + font-size: 0.83rem; + color: rgba(255, 255, 255, 0.66); } -.activity-meta { - display: flex; - gap: var(--space-sm); - align-items: center; - justify-content: space-between; +.dependency-git-identity-field input { + padding: 10px 13px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.16); + background: rgba(0, 0, 0, 0.28); + color: #fff; + font-family: var(--font-sans); + font-size: 0.98rem; + transition: border-color 0.14s ease, background-color 0.14s ease, box-shadow 0.14s ease; } -.activity-actions { - display: flex; - gap: var(--space-xs); - align-items: center; - margin-left: auto; +.dependency-git-identity-field input:focus { + outline: none; + border-color: #388bfd; + background: rgba(0, 0, 0, 0.38); + box-shadow: 0 0 0 2px rgba(56, 139, 253, 0.2); } -.activity-action-btn { - padding: 4px 8px; - font-size: 0.75rem; - line-height: 1; +.dependency-gh-login-helper-text { + margin-bottom: 10px; + font-size: 0.93rem; + color: rgba(255, 255, 255, 0.82); } -.activity-time { - color: var(--text-muted); - font-size: 0.8rem; +.dependency-onboarding-command-wrap { + margin: 18px 0; + padding: 14px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.09); + background: rgba(0, 0, 0, 0.32); } -.activity-kind { +.dependency-onboarding-command-wrap pre { + margin: 0; + color: #a5d6ff; font-family: var(--font-mono); - font-size: 0.8rem; - padding: 2px 8px; - border-radius: 999px; - border: 1px solid transparent; - background: var(--bg-tertiary); - color: var(--text-secondary); + font-size: 0.83rem; + white-space: pre-wrap; } -.activity-kind-agent { - border-color: var(--accent-primary); - color: var(--accent-primary); - background: rgba(31, 111, 235, 0.12); +.dependency-gh-login-code-wrap { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; } -.activity-kind-session { - border-color: var(--accent-success); - color: var(--accent-success); - background: rgba(63, 185, 80, 0.12); +.dependency-gh-login-helper-actions { + margin-top: 10px; } -.activity-kind-server { - border-color: var(--accent-warning); - color: var(--accent-warning); - background: rgba(210, 153, 34, 0.12); +.dependency-gh-login-code { + padding: 8px 14px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(0, 0, 0, 0.5); + font-size: 1.1rem; + letter-spacing: 2px; + color: #fff; } -.activity-kind-git { - border-color: var(--accent-success); - color: var(--accent-success); - background: rgba(63, 185, 80, 0.12); +.onboarding-close-btn { + position: absolute; + top: 18px; + right: 18px; + z-index: 100; + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.78); + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; } -.activity-kind-pr { - border-color: var(--accent-primary); - color: var(--accent-primary); - background: rgba(31, 111, 235, 0.12); +.onboarding-close-btn:hover { + background: rgba(255, 255, 255, 0.18); + border-color: rgba(255, 255, 255, 0.22); + color: #fff; } -.activity-kind-tests { - border-color: var(--accent-warning); - color: var(--accent-warning); - background: rgba(210, 153, 34, 0.12); +.onboarding-close-btn.hidden { + display: none; } -.activity-kind-build { - border-color: var(--accent-warning); - color: var(--accent-warning); - background: rgba(210, 153, 34, 0.12); -} +@media (max-width: 800px) { + .onboarding-overlay { + padding: 12px; + } -.activity-summary { - margin-top: var(--space-sm); - color: var(--text-primary); - font-size: 0.95rem; + .onboarding-container { + padding: 20px; + max-height: calc(100vh - 24px); + } + + .onboarding-step-card { + padding: 20px; + } + + .onboarding-nav-row { + gap: 10px; + flex-wrap: wrap; + } + + .onboarding-btn-back, + .onboarding-btn-primary { + flex: 1 1 100%; + justify-content: center; + } } -.activity-data { - margin-top: var(--space-sm); - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 0.8rem; - white-space: pre-wrap; - word-break: break-word; +@media (prefers-reduced-motion: reduce) { + .onboarding-overlay, + .onboarding-container, + .stepper-diamond, + .onboarding-step-card, + .onboarding-btn-secondary, + .onboarding-btn-back, + .onboarding-btn-primary, + .dependency-git-identity-field input, + .onboarding-close-btn { + animation: none !important; + transition: none !important; + } } diff --git a/client/styles/tabs.css b/client/styles/tabs.css index 970f58cc..044628f4 100644 --- a/client/styles/tabs.css +++ b/client/styles/tabs.css @@ -11,6 +11,8 @@ z-index: 100; overflow-x: auto; overflow-y: hidden; + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); } .workspace-tabs { @@ -233,20 +235,27 @@ /* Scrollbar for tab overflow */ .workspace-tabs-container::-webkit-scrollbar { - height: 4px; + height: var(--scrollbar-size); } .workspace-tabs-container::-webkit-scrollbar-track { - background: var(--bg-secondary); + background: var(--scrollbar-track); + border-radius: var(--scrollbar-radius); } .workspace-tabs-container::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 2px; + background: var(--scrollbar-thumb); + border-radius: var(--scrollbar-radius); + border: 2px solid var(--scrollbar-thumb-border); + background-clip: padding-box; } .workspace-tabs-container::-webkit-scrollbar-thumb:hover { - background: var(--text-secondary); + background: var(--scrollbar-thumb-hover); +} + +.workspace-tabs-container::-webkit-scrollbar-thumb:active { + background: var(--scrollbar-thumb-active); } /* Mobile/Small Screen Adjustments */ diff --git a/server/diagnosticsService.js b/server/diagnosticsService.js index 7021c243..3761cc3e 100644 --- a/server/diagnosticsService.js +++ b/server/diagnosticsService.js @@ -1,22 +1,66 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); -const util = require('util'); -const { execFile } = require('child_process'); - -const execFileAsync = util.promisify(execFile); +const { spawn } = require('child_process'); + +const IS_WIN = process.platform === 'win32'; +const CREATE_NO_WINDOW = 0x08000000; + +function execQuiet(command, args, options = {}) { + const timeout = Number(options.timeout) || 2500; + const maxBuffer = options.maxBuffer || 1024 * 1024; + return new Promise((resolve, reject) => { + const cmdStr = String(command || '').trim(); + const argsArr = Array.isArray(args) ? args : []; + // On Windows, route .cmd/.bat through cmd.exe directly to avoid retry flashing + let spawnCmd = cmdStr; + let spawnArgs = argsArr; + if (IS_WIN && /\.(cmd|bat)$/i.test(cmdStr)) { + spawnCmd = 'cmd.exe'; + spawnArgs = ['/d', '/c', cmdStr, ...argsArr]; + } + const child = spawn(spawnCmd, spawnArgs, { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {}) + }); + let stdout = ''; + let stderr = ''; + let killed = false; + const timer = setTimeout(() => { killed = true; child.kill(); }, timeout); + child.stdout.on('data', (d) => { + stdout += d; + if (stdout.length > maxBuffer) { killed = true; child.kill(); } + }); + child.stderr.on('data', (d) => { + stderr += d; + if (stderr.length > maxBuffer) { killed = true; child.kill(); } + }); + child.on('error', (err) => { clearTimeout(timer); reject(err); }); + child.on('close', (code) => { + clearTimeout(timer); + if (killed) return reject(Object.assign(new Error('TIMEOUT'), { code: 'ETIMEDOUT' })); + if (code !== 0) return reject(Object.assign(new Error(stderr || `Exit code ${code}`), { code: 'EXIT', exitCode: code })); + resolve({ stdout, stderr }); + }); + }); +} async function checkCommand(command, args, options = {}) { const timeout = Number(options.timeoutMs) || 2500; try { - const { stdout, stderr } = await execFileAsync(command, args, { - timeout, - windowsHide: true, - maxBuffer: 1024 * 1024 - }); + const result = await execQuiet(command, args, { timeout, maxBuffer: 1024 * 1024 }); + + const { stdout, stderr } = result || {}; const output = String(stdout || stderr || '').trim(); const firstLine = output.split(/\r?\n/).find(Boolean) || ''; - return { ok: true, command, args, version: firstLine || null }; + return { + ok: true, + command, + args, + version: firstLine || null, + output: output || null + }; } catch (error) { const code = error?.code || null; const message = String(error?.message || error || '').trim(); @@ -35,6 +79,86 @@ async function checkFirstAvailable(candidates) { return await checkCommand(last.command, last.args, last.options); } +function uniqueCommandCandidates(candidates = []) { + const seen = new Set(); + const out = []; + for (const candidate of candidates) { + const command = String(candidate?.command || '').trim(); + if (!command) continue; + const args = Array.isArray(candidate?.args) ? candidate.args : []; + const key = `${command}::${JSON.stringify(args)}`; + if (seen.has(key)) continue; + seen.add(key); + out.push({ command, args, options: candidate?.options }); + } + return out; +} + +async function checkNpmGlobalPackage(npmCommand, packageName) { + const npm = String(npmCommand || '').trim(); + const pkg = String(packageName || '').trim(); + if (!npm || !pkg) { + return { ok: false, error: 'Missing npm command or package name' }; + } + + const res = await checkCommand(npm, ['list', '-g', pkg, '--depth=0'], { timeoutMs: 7000 }); + const combined = String(res?.output || res?.version || '').trim(); + const pkgPattern = new RegExp(`${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@([^\\s]+)`, 'i'); + const versionMatch = combined.match(pkgPattern); + if (!res.ok || !versionMatch?.[1]) { + return { + ok: false, + command: npm, + args: ['list', '-g', pkg, '--depth=0'], + error: String(res?.error || `Package ${pkg} not found in npm global list`) + }; + } + + return { + ok: true, + command: `npm-global:${pkg}`, + args: ['list', '-g', pkg, '--depth=0'], + version: `${pkg}@${versionMatch[1]} (npm global)` + }; +} + +async function checkGitIdentity(gitCommand, gitInstalled) { + const command = String(gitCommand || 'git').trim() || 'git'; + if (!gitInstalled) { + return { + ok: false, + command, + args: ['config', '--global', '--get', 'user.name'], + error: 'Git is not installed' + }; + } + + const nameCheck = await checkCommand(command, ['config', '--global', '--get', 'user.name']); + const emailCheck = await checkCommand(command, ['config', '--global', '--get', 'user.email']); + const name = String(nameCheck?.version || '').trim(); + const email = String(emailCheck?.version || '').trim(); + + if (name && email) { + return { + ok: true, + command, + args: ['config', '--global', '--get', 'user.name,user.email'], + version: `${name} <${email}>` + }; + } + + const missing = []; + if (!name) missing.push('user.name'); + if (!email) missing.push('user.email'); + + return { + ok: false, + command, + args: ['config', '--global', '--get', 'user.name,user.email'], + error: `Missing global Git setting(s): ${missing.join(', ')}` + }; +} + function findTool(tools, id) { if (!Array.isArray(tools)) return null; return tools.find((tool) => String(tool?.id || '') === String(id || '')) || null; @@ -86,47 +210,128 @@ async function collectDiagnostics() { const tools = []; + const nodeCandidates = uniqueCommandCandidates([ + { command: 'node', args: ['--version'] }, + { command: platform === 'win32' ? 'node.exe' : 'node', args: ['--version'] }, + platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'node.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'node.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'node.exe'), args: ['--version'] } : null, + { command: process.execPath || 'node', args: ['--version'] } + ]); + const nodeCheck = await checkFirstAvailable(nodeCandidates); + const nodeCommand = String(nodeCheck?.command || '').trim(); + const nodeDir = nodeCommand ? path.dirname(nodeCommand) : ''; + + const npmCandidates = uniqueCommandCandidates([ + { command: platform === 'win32' ? 'npm.cmd' : 'npm', args: ['--version'] }, + platform === 'win32' ? { command: 'npm', args: ['--version'] } : null, + platform === 'win32' && nodeDir ? { command: path.join(nodeDir, 'npm.cmd'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'npm.cmd'), args: ['--version'] } : null + ]); + const npmCheck = await checkFirstAvailable(npmCandidates); + tools.push({ id: 'node', name: 'Node.js', - ...(await checkCommand(process.execPath || 'node', ['--version'])) + ...nodeCheck }); tools.push({ id: 'npm', name: 'npm', - ...(await checkCommand(platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'])) + ...npmCheck }); + const gitCandidates = uniqueCommandCandidates([ + { command: 'git', args: ['--version'] }, + platform === 'win32' ? { command: 'git.exe', args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null + ]); + tools.push({ id: 'git', name: 'Git', - ...(await checkCommand('git', ['--version'])) + ...(await checkFirstAvailable(gitCandidates)) + }); + const gitTool = tools[tools.length - 1]; + tools.push({ + id: 'gitIdentity', + name: 'Git identity', + ...(await checkGitIdentity(gitTool?.command, !!gitTool?.ok)) }); + const ghCandidates = uniqueCommandCandidates([ + { command: 'gh', args: ['--version'] }, + platform === 'win32' ? { command: 'gh.exe', args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null + ]); + const ghCheck = await checkFirstAvailable(ghCandidates); tools.push({ id: 'gh', name: 'GitHub CLI', - ...(await checkCommand('gh', ['--version'])) + ...ghCheck }); // Auth status is the most common root cause of "0 files/commits" in PR tooling on Windows. // We keep it lightweight: first line of `gh auth status` is enough to spot "not logged in". + const ghAuthCheck = ghCheck?.ok + ? await checkCommand(String(ghCheck.command || 'gh'), ['auth', 'status']) + : { + ok: false, + command: String(ghCheck?.command || 'gh'), + args: ['auth', 'status'], + error: 'GitHub CLI is not installed' + }; tools.push({ id: 'ghAuth', name: 'GitHub CLI auth', - ...(await checkCommand('gh', ['auth', 'status'])) + ...ghAuthCheck }); + const claudeCandidates = uniqueCommandCandidates([ + { command: 'claude', args: ['--version'] }, + platform === 'win32' ? { command: 'claude.cmd', args: ['--version'] } : null, + platform === 'win32' ? { command: 'claude.exe', args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'claude.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.claude', 'local', 'claude.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Claude', 'claude.exe'), args: ['--version'] } : null + ]); tools.push({ id: 'claude', name: 'Claude Code', - ...(await checkCommand('claude', ['--version'])) + ...(await checkFirstAvailable(claudeCandidates)) }); + const codexCandidates = uniqueCommandCandidates([ + { command: 'codex', args: ['--version'] }, + platform === 'win32' ? { command: 'codex.cmd', args: ['--version'] } : null, + platform === 'win32' ? { command: 'codex.exe', args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex.cmd'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'codex.exe'), args: ['--version'] } : null, + platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'codex.exe'), args: ['--version'] } : null + ]); + let codexCheck = await checkFirstAvailable(codexCandidates); + if (!codexCheck?.ok && npmCheck?.ok) { + const npmPackageCheck = await checkNpmGlobalPackage(String(npmCheck.command || '').trim(), '@openai/codex'); + if (npmPackageCheck?.ok) { + codexCheck = npmPackageCheck; + } + } tools.push({ id: 'codex', name: 'Codex CLI', - ...(await checkCommand('codex', ['--version'])) + ...codexCheck }); tools.push({ @@ -568,11 +773,9 @@ async function runFirstRunRepair({ action, rootDir, homeDir } = {}) { } if (actionId === 'rebuild-node-pty') { - const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; - const { stdout, stderr } = await execFileAsync(npmCmd, ['rebuild', 'node-pty'], { - cwd: resolvedRoot, + const npmCmd = IS_WIN ? 'npm.cmd' : 'npm'; + const { stdout, stderr } = await execQuiet(npmCmd, ['rebuild', 'node-pty'], { timeout: 180000, - windowsHide: true, maxBuffer: 4 * 1024 * 1024 }); const output = String(stdout || stderr || '').trim(); diff --git a/server/index.js b/server/index.js index e3a9a62b..b444dd5c 100644 --- a/server/index.js +++ b/server/index.js @@ -99,6 +99,14 @@ const voiceCommandService = require('./voiceCommandService'); const whisperService = require('./whisperService'); const sessionRecoveryService = require('./sessionRecoveryService'); const { collectDiagnostics, collectFirstRunDiagnostics, collectInstallWizard, runFirstRunRepair, runFirstRunSafeRepairs } = require('./diagnosticsService'); +const { + getSetupActions, + runSetupAction, + getSetupActionRun, + getLatestSetupActionRun, + configureGitIdentity +} = require('./setupActionService'); +const { OnboardingStateService } = require('./onboardingStateService'); const { PluginLoaderService } = require('./pluginLoaderService'); const { SchedulerService } = require('./schedulerService'); const { PagerService } = require('./pagerService'); @@ -203,6 +211,20 @@ app.use((req, res, next) => { }); // Define specific routes BEFORE static file serving +app.get('/bootstrap/setup-state.js', (req, res) => { + try { + const state = onboardingStateService.getDependencySetupState(); + res.type('application/javascript'); + res.set('Cache-Control', 'no-store'); + res.send(`window.__ORCHESTRATOR_SETUP_STATE__ = ${JSON.stringify(state)};`); + } catch (error) { + logger.error('Failed to serve setup bootstrap state', { error: error.message, stack: error.stack }); + res.type('application/javascript'); + res.set('Cache-Control', 'no-store'); + res.send('window.__ORCHESTRATOR_SETUP_STATE__ = null;'); + } +}); + // Serve the UI as default app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../client/index.html')); @@ -295,6 +317,7 @@ const processTaskService = ProcessTaskService.getInstance({ sessionManager, work const taskRecordService = TaskRecordService.getInstance(); const userSettingsService = UserSettingsService.getInstance(); const licenseService = LicenseService.getInstance(); +const onboardingStateService = OnboardingStateService.getInstance({ logger }); const proOnly = requirePro(licenseService); const processStatusService = ProcessStatusService.getInstance({ processTaskService, taskRecordService, sessionManager, workspaceManager, userSettingsService }); const processTelemetryService = ProcessTelemetryService.getInstance({ taskRecordService }); @@ -400,6 +423,7 @@ sessionManager.setGitHelper(gitHelper); // Initialize workspace system let workspaceInitialized = false; +let workspaceSystemReady = null; async function initializeWorkspaceSystem() { try { logger.info('Initializing workspace system...'); @@ -425,21 +449,33 @@ async function initializeWorkspaceSystem() { } // Initialize workspace system before starting server -initializeWorkspaceSystem().then(() => { - logger.info('Workspace system initialized'); - loadPlugins() - .then((status) => { - logger.info('Plugin loader finished', { - loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0, - failed: Array.isArray(status?.failed) ? status.failed.length : 0 +workspaceSystemReady = initializeWorkspaceSystem() + .then(() => { + logger.info('Workspace system initialized'); + return true; + }) + .catch(error => { + logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack }); + return false; + }); + +workspaceSystemReady + .then((workspaceReady) => { + if (!workspaceReady) return null; + return loadPlugins() + .then((status) => { + logger.info('Plugin loader finished', { + loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0, + failed: Array.isArray(status?.failed) ? status.failed.length : 0 + }); + return status; + }) + .catch((error) => { + logger.error('Plugin loader failed', { error: error.message, stack: error.stack }); + return null; }); - }) - .catch((error) => { - logger.error('Plugin loader failed', { error: error.message, stack: error.stack }); - }); -}).catch(error => { - logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack }); -}); + }) + .catch(() => null); // WebSocket connection handling io.on('connection', (socket) => { @@ -2303,9 +2339,7 @@ app.post('/api/files/sync', async (req, res) => { }); app.get('/api/process/performance', async (req, res) => { - const { execFile } = require('child_process'); - const util = require('util'); - const execFileAsync = util.promisify(execFile); + const { spawn: spawnProc } = require('child_process'); const isWin = process.platform === 'win32'; const parseIntSafe = (s) => { @@ -2313,15 +2347,27 @@ app.get('/api/process/performance', async (req, res) => { return Number.isFinite(n) ? Math.round(n) : null; }; + const spawnQuiet = (cmd, args, timeout = 1500) => new Promise((resolve) => { + const child = spawnProc(cmd, args, { + stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, + ...(isWin ? { creationFlags: 0x08000000 } : {}) + }); + let out = ''; + child.stdout.on('data', (d) => { out += d; }); + const timer = setTimeout(() => { child.kill(); resolve(''); }, timeout); + child.on('close', () => { clearTimeout(timer); resolve(out); }); + child.on('error', () => { clearTimeout(timer); resolve(''); }); + }); + const getChildPids = async (pid) => { const p = Number(pid); if (!Number.isFinite(p) || p <= 0) return []; try { if (isWin) { - const { stdout } = await execFileAsync( + const stdout = await spawnQuiet( 'powershell.exe', - ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${p}").ProcessId`], - { timeout: 1500, windowsHide: true } + ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${p}").ProcessId`] ); return String(stdout || '') .split(/\s+/) @@ -2329,7 +2375,7 @@ app.get('/api/process/performance', async (req, res) => { .filter(n => Number.isFinite(n) && n > 0); } - const { stdout } = await execFileAsync('pgrep', ['-P', String(p)], { timeout: 1500, windowsHide: true }); + const stdout = await spawnQuiet('pgrep', ['-P', String(p)]); return String(stdout || '') .split('\n') .map(l => parseIntSafe(l)) @@ -2344,17 +2390,16 @@ app.get('/api/process/performance', async (req, res) => { if (!Number.isFinite(p) || p <= 0) return null; try { if (isWin) { - const { stdout } = await execFileAsync( + const stdout = await spawnQuiet( 'powershell.exe', - ['-NoProfile', '-Command', `(Get-Process -Id ${p} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty WorkingSet64)`], - { timeout: 1500, windowsHide: true } + ['-NoProfile', '-Command', `(Get-Process -Id ${p} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty WorkingSet64)`] ); const bytes = Number(String(stdout || '').trim()); if (!Number.isFinite(bytes) || bytes <= 0) return null; return Math.round(bytes / 1024); } - const { stdout } = await execFileAsync('ps', ['-o', 'rss=', '-p', String(p)], { timeout: 1500, windowsHide: true }); + const stdout = await spawnQuiet('ps', ['-o', 'rss=', '-p', String(p)]); return parseIntSafe(stdout); } catch { return null; @@ -4050,6 +4095,144 @@ app.get('/api/lifecycle/policy', (req, res) => { } }); +// Setup helper actions for first-run dependency wizard. +app.get('/api/setup-actions', (req, res) => { + try { + const platform = process.platform; + const actions = getSetupActions(platform); + res.json({ ok: true, platform, actions }); + } catch (error) { + logger.error('Failed to get setup actions', { error: error.message, stack: error.stack }); + res.status(500).json({ ok: false, error: 'Failed to get setup actions' }); + } +}); + +app.get('/api/setup-actions/state', (req, res) => { + try { + const state = onboardingStateService.getDependencySetupState(); + res.json({ ok: true, state }); + } catch (error) { + logger.error('Failed to get setup action state', { error: error.message, stack: error.stack }); + res.status(500).json({ ok: false, error: 'Failed to get setup action state' }); + } +}); + +app.put('/api/setup-actions/state', express.json(), (req, res) => { + try { + const patch = (req.body && typeof req.body === 'object') ? req.body : {}; + const state = onboardingStateService.updateDependencySetupState(patch); + res.json({ ok: true, state }); + } catch (error) { + logger.error('Failed to update setup action state', { error: error.message, stack: error.stack }); + res.status(500).json({ ok: false, error: 'Failed to update setup action state' }); + } +}); + +app.post('/api/setup-actions/run', requirePolicyAction('write'), express.json(), (req, res) => { + try { + const actionId = String(req.body?.actionId || '').trim(); + if (!actionId) { + return res.status(400).json({ ok: false, error: 'actionId is required' }); + } + + const result = runSetupAction(actionId, process.platform); + res.json({ ok: true, ...result }); + } catch (error) { + const code = String(error?.code || ''); + const status = (code === 'unsupported_platform' || code === 'unknown_action' || code === 'not_runnable') ? 400 : 500; + logger.error('Failed to run setup action', { actionId: req.body?.actionId, error: error.message, stack: error.stack }); + res.status(status).json({ ok: false, error: String(error?.message || 'Failed to run setup action') }); + } +}); + +app.post('/api/setup-actions/configure-git-identity', requirePolicyAction('write'), express.json(), async (req, res) => { + try { + const name = String(req.body?.name || '').trim(); + const email = String(req.body?.email || '').trim(); + const result = await configureGitIdentity({ name, email }, process.platform); + res.json({ ok: true, ...result }); + } catch (error) { + const code = String(error?.code || ''); + const status = ( + code === 'unsupported_platform' + || code === 'invalid_input' + || code === 'missing_git' + || code === 'verify_failed' + ) ? 400 : 500; + logger.error('Failed to configure git identity', { + error: error.message, + stack: error.stack + }); + res.status(status).json({ ok: false, error: String(error?.message || 'Failed to configure git identity') }); + } +}); + +app.get('/api/setup-actions/run-status', (req, res) => { + try { + const runId = String(req.query?.runId || '').trim(); + const actionId = String(req.query?.actionId || '').trim(); + const run = runId ? getSetupActionRun(runId) : getLatestSetupActionRun(actionId); + if (!run) { + return res.status(404).json({ ok: false, error: 'Setup action run not found' }); + } + res.json({ ok: true, run }); + } catch (error) { + logger.error('Failed to get setup action run status', { + runId: req.query?.runId, + actionId: req.query?.actionId, + error: error.message, + stack: error.stack + }); + res.status(500).json({ ok: false, error: 'Failed to get setup action run status' }); + } +}); + +app.post('/api/setup-actions/open-url', requirePolicyAction('write'), express.json(), (req, res) => { + try { + const rawUrl = String(req.body?.url || '').trim(); + if (!rawUrl) { + return res.status(400).json({ ok: false, error: 'url is required' }); + } + + let parsed; + try { + parsed = new URL(rawUrl); + } catch { + return res.status(400).json({ ok: false, error: 'Invalid URL' }); + } + + if (!['http:', 'https:'].includes(String(parsed.protocol || '').toLowerCase())) { + return res.status(400).json({ ok: false, error: 'Only http/https URLs are supported' }); + } + + const targetUrl = parsed.toString(); + const { execFile } = require('child_process'); + + const finish = (error) => { + if (error) { + logger.error('Failed to open setup URL', { url: targetUrl, error: error.message, stack: error.stack }); + return res.status(500).json({ ok: false, error: 'Failed to open URL' }); + } + res.json({ ok: true, opened: targetUrl }); + }; + + if (process.platform === 'win32') { + execFile('explorer.exe', [targetUrl], { windowsHide: true }, finish); + return; + } + + if (process.platform === 'darwin') { + execFile('open', [targetUrl], { windowsHide: true }, finish); + return; + } + + execFile('xdg-open', [targetUrl], { windowsHide: true }, finish); + } catch (error) { + logger.error('Failed to open setup URL', { error: error.message, stack: error.stack }); + res.status(500).json({ ok: false, error: 'Failed to open URL' }); + } +}); + // Port registry API endpoints app.get('/api/ports', (req, res) => { try { @@ -7836,7 +8019,13 @@ httpServer.listen(PORT, HOST, () => { } })(); - sessionManager.initializeSessions() + workspaceSystemReady + .then((workspaceReady) => { + if (!workspaceReady) { + return; + } + return sessionManager.initializeSessions(); + }) .then(() => { if (!shouldAutoEnsureDiscordServices) return; // Donโ€™t block server startup; just best-effort keep Services running after restarts. diff --git a/server/onboardingStateService.js b/server/onboardingStateService.js new file mode 100644 index 00000000..a6d43031 --- /dev/null +++ b/server/onboardingStateService.js @@ -0,0 +1,120 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const STATE_VERSION = 1; + +class OnboardingStateService { + constructor({ logger = console, storePath = null } = {}) { + this.logger = logger; + this.storePath = storePath ? path.resolve(String(storePath)) : this.resolveStorePath(); + } + + static getInstance(options = {}) { + if (!OnboardingStateService.instance) { + OnboardingStateService.instance = new OnboardingStateService(options); + } + return OnboardingStateService.instance; + } + + resolveStorePath() { + const dataDirRaw = String(process.env.ORCHESTRATOR_DATA_DIR || '').trim(); + const baseDir = dataDirRaw ? path.resolve(dataDirRaw) : path.join(os.homedir(), '.orchestrator'); + try { + if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true }); + } catch { + // ignore + } + return path.join(baseDir, 'onboarding-state.json'); + } + + getDefaultState() { + return { + version: STATE_VERSION, + updatedAt: null, + dependencySetup: { + completed: false, + dismissed: false, + currentStep: 0, + skippedActionIds: [] + } + }; + } + + normalizeSkippedActionIds(value) { + if (!Array.isArray(value)) return []; + const seen = new Set(); + const result = []; + for (const rawId of value) { + const id = String(rawId || '').trim(); + if (!id || seen.has(id)) continue; + seen.add(id); + result.push(id); + } + return result; + } + + normalizeDependencySetupState(value) { + const next = (value && typeof value === 'object') ? value : {}; + const currentStepRaw = Number.parseInt(String(next.currentStep ?? 0), 10); + return { + completed: next.completed === true, + dismissed: next.dismissed === true, + currentStep: Number.isFinite(currentStepRaw) && currentStepRaw >= 0 ? currentStepRaw : 0, + skippedActionIds: this.normalizeSkippedActionIds(next.skippedActionIds) + }; + } + + loadState() { + const defaults = this.getDefaultState(); + try { + if (!fs.existsSync(this.storePath)) { + return defaults; + } + const parsed = JSON.parse(fs.readFileSync(this.storePath, 'utf8')); + return { + version: STATE_VERSION, + updatedAt: typeof parsed?.updatedAt === 'string' ? parsed.updatedAt : null, + dependencySetup: this.normalizeDependencySetupState(parsed?.dependencySetup) + }; + } catch (error) { + this.logger.warn?.('Failed to load onboarding state', { + path: this.storePath, + error: error.message + }); + return defaults; + } + } + + saveState(state) { + const normalized = { + version: STATE_VERSION, + updatedAt: new Date().toISOString(), + dependencySetup: this.normalizeDependencySetupState(state?.dependencySetup) + }; + const dir = path.dirname(this.storePath); + fs.mkdirSync(dir, { recursive: true }); + const tmpPath = `${this.storePath}.tmp`; + fs.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2)); + fs.renameSync(tmpPath, this.storePath); + return normalized; + } + + getDependencySetupState() { + return this.loadState().dependencySetup; + } + + updateDependencySetupState(patch = {}) { + const current = this.loadState(); + const next = { + ...current, + dependencySetup: this.normalizeDependencySetupState({ + ...(current?.dependencySetup || {}), + ...((patch && typeof patch === 'object') ? patch : {}) + }) + }; + return this.saveState(next).dependencySetup; + } +} + +module.exports = { OnboardingStateService }; diff --git a/server/sessionManager.js b/server/sessionManager.js index d439f289..f9efc646 100644 --- a/server/sessionManager.js +++ b/server/sessionManager.js @@ -2357,15 +2357,27 @@ class SessionManager extends EventEmitter { checkProcessLimit(session) { if (!session.pty || !session.pty.pid) return; - + const pid = Number(session.pty.pid); if (!Number.isFinite(pid) || pid <= 0) return; + const { spawn } = require('child_process'); + if (process.platform === 'win32') { - const { execFile } = require('child_process'); const psCmd = `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}").Count`; - execFile('powershell.exe', ['-NoProfile', '-Command', psCmd], { timeout: 2000 }, (err, stdout) => { - if (err) return; + const child = spawn('powershell.exe', ['-NoProfile', '-Command', psCmd], { + stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, + creationFlags: 0x08000000 // CREATE_NO_WINDOW + }); + let stdout = ''; + child.stdout.on('data', (d) => { stdout += d; }); + const timer = setTimeout(() => child.kill(), 2000); + child.on('error', () => { + clearTimeout(timer); + }); + child.on('close', () => { + clearTimeout(timer); const processCount = parseInt(String(stdout || '').trim(), 10); if (!Number.isFinite(processCount)) return; if (processCount > this.maxProcessesPerSession) { @@ -2381,10 +2393,20 @@ class SessionManager extends EventEmitter { } // POSIX: use pgrep to count child processes without shell interpolation. - const { execFile } = require('child_process'); - execFile('pgrep', ['-P', String(pid)], { timeout: 2000, windowsHide: true }, (err, stdout) => { + const child = spawn('pgrep', ['-P', String(pid)], { + stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true + }); + let stdout = ''; + child.stdout.on('data', (d) => { stdout += d; }); + const timer = setTimeout(() => child.kill(), 2000); + child.on('error', () => { + clearTimeout(timer); + }); + child.on('close', (code) => { + clearTimeout(timer); // pgrep exits with code 1 when no child process matches; treat as zero children. - if (err && Number(err?.code) !== 1) return; + if (code !== 0 && code !== 1) return; const lines = String(stdout || '') .split(/\r?\n/) .map((line) => line.trim()) @@ -2392,7 +2414,7 @@ class SessionManager extends EventEmitter { const processCount = lines.length; if (!Number.isFinite(processCount)) return; if (processCount > this.maxProcessesPerSession) { - logger.error('Process limit exceeded', { + logger.error('Process limit exceeded', { sessionId: session.id, processCount, limit: this.maxProcessesPerSession @@ -2468,13 +2490,13 @@ class SessionManager extends EventEmitter { if (!Number.isFinite(numericPid) || numericPid <= 0) return; if (process.platform === 'win32') { - const { execFile } = require('child_process'); - execFile( - 'taskkill', - ['/PID', String(numericPid), '/T', '/F'], - { windowsHide: true, timeout: 2500 }, - () => {} - ); + const { spawn: spawnProc } = require('child_process'); + const child = spawnProc('taskkill', ['/PID', String(numericPid), '/T', '/F'], { + stdio: 'ignore', + windowsHide: true, + creationFlags: 0x08000000 + }); + child.on('error', () => {}); return; } diff --git a/server/setupActionService.js b/server/setupActionService.js new file mode 100644 index 00000000..4ad5e234 --- /dev/null +++ b/server/setupActionService.js @@ -0,0 +1,568 @@ +const crypto = require('crypto'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const { spawn } = require('child_process'); + +const IS_WIN = process.platform === 'win32'; +const CREATE_NO_WINDOW = 0x08000000; + +function execQuiet(command, args, options = {}) { + const timeout = Number(options.timeout) || 3000; + const maxBuffer = options.maxBuffer || 1024 * 1024; + return new Promise((resolve, reject) => { + const cmdStr = String(command || '').trim(); + const argsArr = Array.isArray(args) ? args : []; + let spawnCmd = cmdStr; + let spawnArgs = argsArr; + if (IS_WIN && /\.(cmd|bat)$/i.test(cmdStr)) { + spawnCmd = 'cmd.exe'; + spawnArgs = ['/d', '/c', cmdStr, ...argsArr]; + } + const child = spawn(spawnCmd, spawnArgs, { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {}) + }); + let stdout = ''; + let stderr = ''; + let killed = false; + const timer = setTimeout(() => { killed = true; child.kill(); }, timeout); + child.stdout.on('data', (d) => { + stdout += d; + if (stdout.length > maxBuffer) { killed = true; child.kill(); } + }); + child.stderr.on('data', (d) => { + stderr += d; + if (stderr.length > maxBuffer) { killed = true; child.kill(); } + }); + child.on('error', (err) => { clearTimeout(timer); reject(err); }); + child.on('close', (code) => { + clearTimeout(timer); + if (killed) return reject(Object.assign(new Error('TIMEOUT'), { code: 'ETIMEDOUT' })); + if (code !== 0) return reject(Object.assign(new Error(stderr || `Exit code ${code}`), { code: 'EXIT', exitCode: code })); + resolve({ stdout, stderr }); + }); + }); +} + +const setupActionRuns = new Map(); +const latestRunByActionId = new Map(); +const MAX_OUTPUT_LINES = 180; +const MAX_RUNS_RETAINED = 50; + +function pruneOldRuns() { + if (setupActionRuns.size <= MAX_RUNS_RETAINED) return; + const toDelete = Array.from(setupActionRuns.keys()).slice(0, setupActionRuns.size - MAX_RUNS_RETAINED); + for (const key of toDelete) { + setupActionRuns.delete(key); + } +} +const GH_LOGIN_CODE_PATTERN = /\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i; +const GH_LOGIN_URL_PATTERN = /https:\/\/github\.com\/login\/device(?:\S*)?/i; +const GH_LOGIN_HINT_PATTERN = /one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i; + +function stripAnsi(value) { + return String(value || '').replace(/\u001b\[[0-9;]*m/g, ''); +} + +function getGhLoginDebugLogPath() { + const customDataDir = String(process.env.ORCHESTRATOR_DATA_DIR || '').trim(); + if (customDataDir) { + return path.join(customDataDir, 'logs', 'gh-login-debug.log'); + } + return path.join(os.tmpdir(), 'orchestrator-gh-login-debug.log'); +} + +function appendGhLoginDebugLog(event, payload = {}) { + const line = JSON.stringify({ + at: new Date().toISOString(), + event: String(event || '').trim() || 'event', + ...(payload && typeof payload === 'object' ? payload : {}) + }); + const logPath = getGhLoginDebugLogPath(); + try { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.appendFileSync(logPath, `${line}\n`, 'utf8'); + } catch { + // Best-effort debug logging; never block setup flow. + } +} + +function uniqueStrings(values = []) { + const seen = new Set(); + const out = []; + values.forEach((value) => { + const item = String(value || '').trim(); + if (!item || seen.has(item)) return; + seen.add(item); + out.push(item); + }); + return out; +} + +async function checkExecutable(command, args = ['--version']) { + const commandStr = String(command || '').trim(); + if (!commandStr) return { ok: false, error: 'Missing command' }; + + try { + await execQuiet(commandStr, Array.isArray(args) ? args : [], { timeout: 3000 }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error: String(error?.message || error || 'Command check failed') + }; + } +} + +function getGitCommandCandidates(platform = process.platform) { + if (platform !== 'win32') { + return ['git']; + } + + return uniqueStrings([ + 'git', + 'git.exe', + path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'), + path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'), + path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'), + path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'), + path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe') + ]); +} + +async function resolveGitCommand(platform = process.platform) { + const candidates = getGitCommandCandidates(platform); + for (const command of candidates) { + const check = await checkExecutable(command, ['--version']); + if (check.ok) return command; + } + return ''; +} + +async function runGitCommand(command, args = []) { + try { + const result = await execQuiet(command, Array.isArray(args) ? args : [], { timeout: 9000 }); + return String(result?.stdout || result?.stderr || ''); + } catch (error) { + const message = String(error?.message || error || 'Git command failed'); + const err = new Error(message); + err.code = String(error?.code || 'git_command_failed'); + throw err; + } +} + +function firstNonEmptyLine(text) { + return String(text || '') + .replace(/\r/g, '') + .split('\n') + .map((line) => line.trim()) + .find(Boolean) || ''; +} + +function getSetupActions(platform = process.platform) { + if (platform !== 'win32') { + return []; + } + + return [ + { + id: 'install-git', + title: 'Git Integration', + description: 'Required for repository and worktree access.', + command: 'winget install --id Git.Git --exact --source winget --accept-source-agreements --accept-package-agreements', + docsUrl: 'https://git-scm.com/download/win', + required: true, + runSupported: true + }, + { + id: 'configure-git-identity', + title: 'Git Identity', + description: 'Set your name and email for accurate commits.', + command: 'git config --global user.name "Your Name"\ngit config --global user.email "you@example.com"', + docsUrl: 'https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup', + required: false, + optional: true, + runSupported: false + }, + { + id: 'install-node', + title: 'Node.js LTS', + description: 'Required core dependency for running agents.', + command: 'winget install --id OpenJS.NodeJS.LTS --exact --source winget --accept-source-agreements --accept-package-agreements', + docsUrl: 'https://nodejs.org/en/download', + required: false, + runSupported: true + }, + { + id: 'install-gh', + title: 'GitHub CLI', + description: 'Optional. Install now, then continue to GitHub login in the next step.', + command: 'winget install --id GitHub.cli --exact --source winget --accept-source-agreements --accept-package-agreements', + docsUrl: 'https://cli.github.com/', + required: false, + optional: true, + runSupported: true + }, + { + id: 'gh-login', + title: 'GitHub Authentication', + description: 'Optional after GitHub CLI install. Sign in to enable PR and repo actions.', + command: [ + "$ErrorActionPreference = 'Stop'", + '$env:NO_COLOR = "1"', + '$env:GH_PAGER = ""', + '$gh = ""', + '$cmd = Get-Command gh -ErrorAction SilentlyContinue', + 'if ($cmd -and $cmd.Source) { $gh = $cmd.Source }', + 'if (-not $gh) {', + ' $candidates = @(', + ' "$env:ProgramFiles\\GitHub CLI\\gh.exe",', + ' "$env:ProgramFiles(x86)\\GitHub CLI\\gh.exe",', + ' "$env:LOCALAPPDATA\\Programs\\GitHub CLI\\gh.exe"', + ' )', + ' foreach ($candidate in $candidates) {', + ' if (Test-Path $candidate) { $gh = $candidate; break }', + ' }', + '}', + 'if (-not $gh) { throw "GitHub CLI executable not found. Install GitHub CLI first." }', + '$prevErrorAction = $ErrorActionPreference', + '$ErrorActionPreference = "Continue"', + '& $gh auth status --hostname github.com *> $null', + '$authStatusExitCode = $LASTEXITCODE', + '$ErrorActionPreference = $prevErrorAction', + 'if ($authStatusExitCode -eq 0) { Write-Output "GitHub CLI is already authenticated."; exit 0 }', + 'Write-Output "Starting GitHub CLI web login..."', + 'Write-Output "Expect a one-time code and https://github.com/login/device below."', + '& $gh auth login --hostname github.com --git-protocol https --web --skip-ssh-key', + 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }' + ].join('\n'), + docsUrl: 'https://cli.github.com/manual/gh_auth_login', + required: false, + optional: true, + runSupported: true + }, + { + id: 'install-claude', + title: 'Claude Code CLI', + description: 'Primary AI agent powered by Anthropic.', + command: 'winget install --id Anthropic.ClaudeCode --exact --source winget --accept-source-agreements --accept-package-agreements', + docsUrl: 'https://docs.claude.com/en/docs/claude-code/setup', + required: false, + optional: true, + runSupported: true + }, + { + id: 'install-codex', + title: 'Codex CLI', + description: 'Alternative AI agent tool for development.', + command: [ + "$ErrorActionPreference = 'Stop'", + '$npm = ""', + '$cmd = Get-Command npm -ErrorAction SilentlyContinue', + 'if ($cmd -and $cmd.Source) { $npm = $cmd.Source }', + 'if (-not $npm) {', + ' $candidates = @(', + ' "$env:ProgramFiles\\nodejs\\npm.cmd",', + ' "$env:ProgramFiles(x86)\\nodejs\\npm.cmd",', + ' "$env:LOCALAPPDATA\\Programs\\nodejs\\npm.cmd",', + ' "$env:APPDATA\\npm\\npm.cmd"', + ' )', + ' foreach ($candidate in $candidates) {', + ' if (Test-Path $candidate) { $npm = $candidate; break }', + ' }', + '}', + 'if (-not $npm) { throw "npm was not found. Install Node.js LTS first, then run this step again." }', + '& $npm install -g @openai/codex', + 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }' + ].join('\n'), + docsUrl: 'https://developers.openai.com/codex/cli', + required: false, + runSupported: true + } + ]; +} + +function getSetupActionById(actionId, platform = process.platform) { + const id = String(actionId || '').trim(); + if (!id) return null; + return getSetupActions(platform).find((action) => action.id === id) || null; +} + +function createRunId(actionId) { + return `setup-${String(actionId || 'action')}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`; +} + +function getRunSummary(run) { + if (!run) return null; + return { + runId: run.runId, + actionId: run.actionId, + title: run.title, + command: run.command, + status: run.status, + startedAt: run.startedAt, + endedAt: run.endedAt || null, + pid: Number.isFinite(run.pid) ? run.pid : null, + exitCode: Number.isInteger(run.exitCode) ? run.exitCode : null, + error: run.error || null, + output: Array.isArray(run.output) ? run.output.slice(-25) : [], + ghDeviceCode: run.ghDeviceCode || null, + ghDeviceUrl: run.ghDeviceUrl || null, + ghHasDeviceHint: !!run.ghHasDeviceHint, + ghDebugLogPath: run.ghDebugLogPath || null, + updatedAt: run.updatedAt || run.startedAt + }; +} + +function appendRunOutput(run, chunk, stream = 'stdout') { + if (!run) return; + const text = String(chunk || ''); + if (!text) return; + const lines = text + .replace(/\r/g, '') + .split('\n') + .map((line) => stripAnsi(line).trimEnd()) + .filter(Boolean); + if (!lines.length) return; + const at = new Date().toISOString(); + lines.forEach((line) => { + const cleanLine = String(line || '').slice(0, 1600); + run.output.push({ at, stream, line: cleanLine }); + if (run.actionId === 'gh-login') { + const codeMatch = cleanLine.match(GH_LOGIN_CODE_PATTERN); + const urlMatch = cleanLine.match(GH_LOGIN_URL_PATTERN); + if (codeMatch?.[1]) run.ghDeviceCode = String(codeMatch[1]).toUpperCase(); + if (urlMatch?.[0]) run.ghDeviceUrl = String(urlMatch[0]).trim(); + if (GH_LOGIN_HINT_PATTERN.test(cleanLine)) run.ghHasDeviceHint = true; + appendGhLoginDebugLog('output', { + runId: run.runId, + stream, + line: cleanLine + }); + } + }); + if (run.output.length > MAX_OUTPUT_LINES) { + run.output.splice(0, run.output.length - MAX_OUTPUT_LINES); + } + run.updatedAt = at; +} + +function launchPowerShellCommand(action) { + const runId = createRunId(action.id); + const run = { + runId, + actionId: action.id, + title: action.title, + command: action.command, + status: 'running', + startedAt: new Date().toISOString(), + endedAt: null, + pid: null, + exitCode: null, + error: null, + output: [], + ghDeviceCode: null, + ghDeviceUrl: null, + ghHasDeviceHint: false, + ghDebugLogPath: action.id === 'gh-login' ? getGhLoginDebugLogPath() : null, + updatedAt: null + }; + run.updatedAt = run.startedAt; + if (action.id === 'gh-login') { + appendGhLoginDebugLog('run_started', { + runId: run.runId, + title: run.title + }); + } + + setupActionRuns.set(runId, run); + latestRunByActionId.set(action.id, runId); + pruneOldRuns(); + + try { + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', String(action.command || '')], + { + detached: false, + windowsHide: true, + stdio: ['ignore', 'pipe', 'pipe'], + ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {}) + } + ); + run.pid = Number.isFinite(child?.pid) ? child.pid : null; + run.updatedAt = new Date().toISOString(); + + child.stdout.on('data', (chunk) => appendRunOutput(run, chunk, 'stdout')); + child.stderr.on('data', (chunk) => appendRunOutput(run, chunk, 'stderr')); + + child.on('error', (error) => { + run.status = 'failed'; + run.error = String(error?.message || error || 'Failed to launch setup action'); + run.endedAt = new Date().toISOString(); + run.updatedAt = run.endedAt; + if (action.id === 'gh-login') { + appendGhLoginDebugLog('run_error', { + runId: run.runId, + error: run.error + }); + } + }); + + child.on('close', (code) => { + run.exitCode = Number.isInteger(code) ? code : null; + run.status = code === 0 ? 'success' : 'failed'; + if (code !== 0 && !run.error) { + run.error = `Setup action exited with code ${String(code)}`; + } + run.endedAt = new Date().toISOString(); + run.updatedAt = run.endedAt; + if (action.id === 'gh-login') { + appendGhLoginDebugLog('run_closed', { + runId: run.runId, + status: run.status, + exitCode: run.exitCode, + error: run.error || null, + parsedCode: run.ghDeviceCode || null, + parsedUrl: run.ghDeviceUrl || null, + sawHint: !!run.ghHasDeviceHint + }); + } + }); + } catch (error) { + run.status = 'failed'; + run.error = String(error?.message || error || 'Failed to launch setup action'); + run.endedAt = new Date().toISOString(); + run.updatedAt = run.endedAt; + if (action.id === 'gh-login') { + appendGhLoginDebugLog('run_launch_failed', { + runId: run.runId, + error: run.error + }); + } + } + + return run; +} + +function getSetupActionRun(runId) { + const key = String(runId || '').trim(); + if (!key) return null; + return getRunSummary(setupActionRuns.get(key)); +} + +function getLatestSetupActionRun(actionId) { + const id = String(actionId || '').trim(); + if (!id) return null; + const runId = latestRunByActionId.get(id); + if (!runId) return null; + return getRunSummary(setupActionRuns.get(runId)); +} + +function runSetupAction(actionId, platform = process.platform) { + if (platform !== 'win32') { + const err = new Error('Setup actions are currently implemented for Windows only.'); + err.code = 'unsupported_platform'; + throw err; + } + + const action = getSetupActionById(actionId, platform); + if (!action) { + const err = new Error(`Unknown setup action: ${String(actionId || '')}`); + err.code = 'unknown_action'; + throw err; + } + + if (!action.runSupported || !action.command) { + const err = new Error(`Action "${action.id}" cannot be launched from the app.`); + err.code = 'not_runnable'; + throw err; + } + + const latestRun = getLatestSetupActionRun(action.id); + if (latestRun && latestRun.status === 'running') { + return { + id: action.id, + title: action.title, + started: true, + alreadyRunning: true, + run: latestRun, + message: `${action.title} is already running.` + }; + } + + const run = launchPowerShellCommand(action); + const runSummary = getRunSummary(run); + + return { + id: action.id, + title: action.title, + started: true, + alreadyRunning: false, + run: runSummary, + message: `Started ${action.title}. Progress updates are now tracked in onboarding.` + }; +} + +async function configureGitIdentity({ name, email } = {}, platform = process.platform) { + if (platform !== 'win32') { + const err = new Error('Git identity setup is currently implemented for Windows only.'); + err.code = 'unsupported_platform'; + throw err; + } + + const normalizedName = String(name || '').trim(); + const normalizedEmail = String(email || '').trim(); + if (!normalizedName || !normalizedEmail) { + const err = new Error('Both name and email are required.'); + err.code = 'invalid_input'; + throw err; + } + + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) { + const err = new Error('Enter a valid email address.'); + err.code = 'invalid_input'; + throw err; + } + + const gitCommand = await resolveGitCommand(platform); + if (!gitCommand) { + const err = new Error('Git is not installed or not available on PATH.'); + err.code = 'missing_git'; + throw err; + } + + await runGitCommand(gitCommand, ['config', '--global', 'user.name', normalizedName]); + await runGitCommand(gitCommand, ['config', '--global', 'user.email', normalizedEmail]); + + const savedName = firstNonEmptyLine(await runGitCommand(gitCommand, ['config', '--global', '--get', 'user.name'])); + const savedEmail = firstNonEmptyLine(await runGitCommand(gitCommand, ['config', '--global', '--get', 'user.email'])); + + if (!savedName || !savedEmail) { + const err = new Error('Git identity was saved, but verification failed.'); + err.code = 'verify_failed'; + throw err; + } + + return { + id: 'configure-git-identity', + title: 'Configure Git identity', + ok: true, + gitCommand, + name: savedName, + email: savedEmail, + summary: `${savedName} <${savedEmail}>`, + message: 'Git identity saved successfully.' + }; +} + +module.exports = { + getSetupActions, + getSetupActionById, + runSetupAction, + getSetupActionRun, + getLatestSetupActionRun, + configureGitIdentity +}; diff --git a/server/workspaceManager.js b/server/workspaceManager.js index 06c2aaf3..35d550db 100644 --- a/server/workspaceManager.js +++ b/server/workspaceManager.js @@ -707,20 +707,31 @@ class WorkspaceManager { // 3. First available workspace // 4. None (show dashboard) - // Don't auto-select workspace - let user choose from dashboard - // if (this.config.activeWorkspace && this.workspaces.has(this.config.activeWorkspace)) { - // this.activeWorkspace = this.workspaces.get(this.config.activeWorkspace); - // logger.info(`Set active workspace from config: ${this.activeWorkspace.name}`); - // return; - // } - - // Don't auto-select first workspace - show dashboard instead - // if (this.workspaces.size > 0) { - // const firstWorkspace = Array.from(this.workspaces.values())[0]; - // this.activeWorkspace = firstWorkspace; - // logger.info(`Set active workspace (first available): ${this.activeWorkspace.name}`); - // return; - // } + const rememberLastWorkspace = this.config?.ui?.rememberLastWorkspace !== false; + const configuredWorkspaceId = String(this.config?.activeWorkspace || '').trim(); + + if (rememberLastWorkspace && configuredWorkspaceId && this.workspaces.has(configuredWorkspaceId)) { + this.activeWorkspace = this.workspaces.get(configuredWorkspaceId); + logger.info(`Set active workspace from config: ${this.activeWorkspace.name}`); + return; + } + + if (rememberLastWorkspace && configuredWorkspaceId && !this.workspaces.has(configuredWorkspaceId)) { + logger.warn(`Configured active workspace missing: ${configuredWorkspaceId}`); + } + + if (rememberLastWorkspace && this.workspaces.size > 0) { + const sorted = Array.from(this.workspaces.values()) + .sort((a, b) => { + const aTime = a.lastAccess ? new Date(a.lastAccess).getTime() : 0; + const bTime = b.lastAccess ? new Date(b.lastAccess).getTime() : 0; + return bTime - aTime; + }); + const firstWorkspace = sorted[0]; + this.activeWorkspace = firstWorkspace; + logger.info(`Set active workspace by fallback: ${this.activeWorkspace.name}`); + return; + } logger.info('No active workspace set (no workspaces available)'); } @@ -1004,7 +1015,7 @@ class WorkspaceManager { enabled: true, count: pairs, namingPattern: 'work{n}', - autoCreate: false + autoCreate: true // auto-create worktrees when workspace is first opened }, terminals: { pairs diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 076c0231..d2b2d184 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,6 +10,14 @@ use uuid::Uuid; use tauri_plugin_updater::UpdaterExt; use url::Url; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; + +#[cfg(target_os = "windows")] +const CREATE_NO_WINDOW: u32 = 0x08000000; +#[cfg(target_os = "windows")] +const DETACHED_PROCESS: u32 = 0x00000008; + mod terminal; mod file_watcher; use terminal::{TerminalManager, TerminalOutput}; @@ -602,6 +610,18 @@ fn main() { cmd.arg(entry); cmd.current_dir(&data_dir); cmd.stdin(Stdio::null()); + // On Windows, null stdout/stderr to avoid console window flash. + // On other platforms, keep stderr for debugging. + #[cfg(target_os = "windows")] + { + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::null()); + } + #[cfg(not(target_os = "windows"))] + { + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::inherit()); + } cmd.env("ORCHESTRATOR_HOST", "127.0.0.1"); cmd.env("ORCHESTRATOR_PORT", port.to_string()); cmd.env("AUTH_TOKEN", token.clone()); @@ -612,6 +632,11 @@ fn main() { cmd.env("AUTO_START_DIFF_VIEWER", "false"); } + #[cfg(target_os = "windows")] + { + cmd.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS); + } + match cmd.spawn() { Err(err) => { let details = format!( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e6e4e6c7..629f0e47 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -40,7 +40,13 @@ "resources/backend/*", "resources/backend/server/*", "resources/backend/client/*", + "resources/backend/node/*", "resources/backend/node_modules" ] + }, + "plugins": { + "updater": { + "pubkey": "" + } } } diff --git a/tests/unit/diagnosticsService.test.js b/tests/unit/diagnosticsService.test.js index 571466fd..ab5c98d8 100644 --- a/tests/unit/diagnosticsService.test.js +++ b/tests/unit/diagnosticsService.test.js @@ -1,36 +1,88 @@ +const { EventEmitter } = require('events'); +const { Readable } = require('stream'); + describe('diagnosticsService platform smoke', () => { afterEach(() => { jest.resetModules(); jest.clearAllMocks(); }); + function fakeSpawn(command, args) { + const child = new EventEmitter(); + const stdout = new Readable({ read() {} }); + const stderr = new Readable({ read() {} }); + child.stdout = stdout; + child.stderr = stderr; + child.kill = () => {}; + + const cmd = String(command || ''); + const argv = Array.isArray(args) ? args.map(String) : []; + + // Resolve .cmd wrappers through cmd.exe + let resolvedCmd = cmd; + if (cmd === 'cmd.exe' && argv[0] === '/d' && argv[1] === '/c') { + resolvedCmd = argv[2] || ''; + } + + process.nextTick(() => { + if (resolvedCmd === process.execPath || resolvedCmd === 'node') { + stdout.push('v22.0.0\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } else if (resolvedCmd === 'npm' || resolvedCmd === 'npm.cmd') { + stdout.push('10.0.0\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } else if (resolvedCmd === 'git' || resolvedCmd === 'git.exe') { + if (argv.includes('user.name')) { + stdout.push('Test User\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } else if (argv.includes('user.email')) { + stdout.push('test@example.com\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } else { + stdout.push('git version 2.44.0\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } + } else if (resolvedCmd === 'gh' && argv.includes('--version')) { + stdout.push('gh version 2.61.0\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } else if (resolvedCmd === 'gh' && argv.includes('auth')) { + stdout.push(null); stderr.push('not logged in\n'); stderr.push(null); + child.emit('close', 1); + } else if (resolvedCmd === 'claude' || resolvedCmd === 'claude.cmd') { + stdout.push(null); stderr.push(null); + child.emit('error', Object.assign(new Error('missing command: claude'), { code: 'ENOENT' })); + } else if (resolvedCmd === 'codex' || resolvedCmd === 'codex.cmd') { + stdout.push(null); stderr.push(null); + child.emit('error', Object.assign(new Error('missing command: codex'), { code: 'ENOENT' })); + } else if (resolvedCmd === 'bash' || resolvedCmd === 'bash.exe' || resolvedCmd === 'powershell.exe') { + stdout.push('shell ok\n'); stdout.push(null); stderr.push(null); + child.emit('close', 0); + } else if (resolvedCmd === 'ffmpeg') { + stdout.push(null); stderr.push(null); + child.emit('error', Object.assign(new Error('missing command: ffmpeg'), { code: 'ENOENT' })); + } else if (resolvedCmd === 'wsl.exe') { + stdout.push(null); stderr.push(null); + child.emit('error', Object.assign(new Error('missing command: wsl.exe'), { code: 'ENOENT' })); + } else { + stdout.push(null); stderr.push(null); + child.emit('error', Object.assign(new Error(`missing command: ${resolvedCmd}`), { code: 'ENOENT' })); + } + }); + + return child; + } + const mockChildProcess = () => { jest.doMock('child_process', () => ({ + spawn: fakeSpawn, execFile: (command, args, options, callback) => { + // Legacy fallback for any code still using execFile const cmd = String(command || ''); - const argv = Array.isArray(args) ? args.map(String) : []; if (cmd === process.execPath || cmd === 'node') return callback(null, 'v22.0.0\n', ''); if (cmd === 'npm' || cmd === 'npm.cmd') return callback(null, '10.0.0\n', ''); if (cmd === 'git') return callback(null, 'git version 2.44.0\n', ''); - if (cmd === 'gh' && argv[0] === '--version') return callback(null, 'gh version 2.61.0\n', ''); - if (cmd === 'gh' && argv[0] === 'auth') { - const err = new Error('not logged in'); - err.code = 1; - return callback(err, '', 'not logged in'); - } - if (cmd === 'claude') { - const err = new Error('missing command: claude'); - err.code = 'ENOENT'; - return callback(err, '', ''); - } - if (cmd === 'codex') { - const err = new Error('missing command: codex'); - err.code = 'ENOENT'; - return callback(err, '', ''); - } - if (cmd === 'bash' || cmd === 'bash.exe' || cmd === 'powershell.exe') { - return callback(null, 'shell ok\n', ''); - } + if (cmd === 'gh') return callback(null, 'gh version 2.61.0\n', ''); + if (cmd === 'bash' || cmd === 'bash.exe' || cmd === 'powershell.exe') return callback(null, 'shell ok\n', ''); const err = new Error(`missing command: ${cmd}`); err.code = 'ENOENT'; return callback(err, '', ''); diff --git a/tests/unit/onboardingStateService.test.js b/tests/unit/onboardingStateService.test.js new file mode 100644 index 00000000..1edbb30f --- /dev/null +++ b/tests/unit/onboardingStateService.test.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { OnboardingStateService } = require('../../server/onboardingStateService'); + +describe('OnboardingStateService', () => { + const logger = { warn: jest.fn() }; + + beforeEach(() => { + logger.warn.mockReset(); + }); + + test('returns default dependency setup state when no file exists', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onboarding-state-default-')); + const storePath = path.join(tempDir, 'onboarding-state.json'); + const service = new OnboardingStateService({ logger, storePath }); + + expect(service.getDependencySetupState()).toEqual({ + completed: false, + dismissed: false, + currentStep: 0, + skippedActionIds: [] + }); + }); + + test('persists normalized dependency setup state across instances', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onboarding-state-persist-')); + const storePath = path.join(tempDir, 'onboarding-state.json'); + const service = new OnboardingStateService({ logger, storePath }); + + const updated = service.updateDependencySetupState({ + completed: true, + dismissed: false, + currentStep: '4', + skippedActionIds: ['install-gh', 'install-gh', ' ', 'install-codex'] + }); + + expect(updated).toEqual({ + completed: true, + dismissed: false, + currentStep: 4, + skippedActionIds: ['install-gh', 'install-codex'] + }); + + const reloaded = new OnboardingStateService({ logger, storePath }); + expect(reloaded.getDependencySetupState()).toEqual(updated); + }); + + test('merges patches without dropping existing dependency setup state', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onboarding-state-merge-')); + const storePath = path.join(tempDir, 'onboarding-state.json'); + const service = new OnboardingStateService({ logger, storePath }); + + service.updateDependencySetupState({ + completed: true, + currentStep: 3, + skippedActionIds: ['install-gh'] + }); + + const updated = service.updateDependencySetupState({ + dismissed: true + }); + + expect(updated).toEqual({ + completed: true, + dismissed: true, + currentStep: 3, + skippedActionIds: ['install-gh'] + }); + }); +});