From f45090ffa1e84c5f6bdbf02d7ec3cd029aad9dd9 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:57:43 +1100 Subject: [PATCH 01/14] fix: clean windows onboarding with reviewed fixes Cherry-pick the windows onboarding flow from AnrokX PRs #796 and #791 with all review issues fixed: Features ported: - First-run dependency wizard (Git, Node, npm, gh, Claude, Codex) - Setup action service with PowerShell command runner - GitHub CLI device-flow login with code detection - Git identity configuration (name/email) - Diagnostics panel enhancements - Toast notification CSS rewrite - Workspace startup race condition fix (workspaceSystemReady gate) - Desktop launch trace support in bootstrap page Issues fixed from review: - Remove dead `guidance` variable (~60 lines never rendered in template) - Add retention limit (50 runs) to setupActionService Maps (memory leak) - Disable updater plugin instead of empty pubkey (prevents silent update failures) - Keep stderr on non-Windows platforms for debugging (was nulled everywhere) - Document autoCreate behavioral change in workspaceManager - Exclude binary artifacts (zip files) and patch script from cherry-pick Based on work by AnrokX in PRs #796 and #791. Co-Authored-By: AnrokX Co-Authored-By: Claude Opus 4.6 --- CODEBASE_DOCUMENTATION.md | 11 + client/app.js | 2223 ++++++++++--- client/index.html | 105 +- client/notifications.js | 6 +- client/styles.css | 5772 +++++++++++++++++++++------------- client/styles/tabs.css | 19 +- server/diagnosticsService.js | 201 +- server/index.js | 163 +- server/setupActionService.js | 552 ++++ server/workspaceManager.js | 41 +- src-tauri/src/main.rs | 25 + src-tauri/tauri.conf.json | 5 + 12 files changed, 6338 insertions(+), 2785 deletions(-) create mode 100644 server/setupActionService.js diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index eadc9144..41397f7b 100644 --- a/CODEBASE_DOCUMENTATION.md +++ b/CODEBASE_DOCUMENTATION.md @@ -538,5 +538,16 @@ 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/index.js - Routes: GET /api/setup-actions, POST /api/setup-actions/run +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..6327b8ba 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,1372 @@ 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 (!isWindowsHost) return; + if (pending) { + body?.classList?.add?.('dependency-onboarding-booting'); + body?.classList?.remove?.('dependency-onboarding-active'); + return; + } + body?.classList?.remove?.('dependency-onboarding-booting'); + }; + setBootstrapPending(true); + 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 state = { + loading: false, + diagnostics: null, + actions: [], + currentStep: 0, + showWelcome: true, + skippedActionIds: new Set(), + actionRuns: new Map(), + actionRunPollers: new Map(), + gitIdentity: { + name: '', + email: '' + }, + gitIdentityHelpVisible: false + }; + + const readDismissed = () => { + try { + return localStorage.getItem(dismissKey) === 'true'; + } catch { + return false; + } + }; + + const writeDismissed = (value) => { + try { + if (value) localStorage.setItem(dismissKey, 'true'); + else localStorage.removeItem(dismissKey); + } catch { + // ignore + } + }; + + const readCompleted = () => { + try { + return localStorage.getItem(completedKey) === 'true'; + } catch { + return false; + } + }; + + const writeCompleted = (value) => { + try { + if (value) localStorage.setItem(completedKey, 'true'); + else localStorage.removeItem(completedKey); + } catch { + // ignore + } + }; + + const readSavedStep = () => { + try { + const raw = Number.parseInt(String(localStorage.getItem(progressKey) || ''), 10); + if (Number.isFinite(raw) && raw >= 0) return raw; + return 0; + } catch { + return 0; + } + }; + + const writeSavedStep = (step) => { + try { + localStorage.setItem(progressKey, String(Math.max(0, Number(step) || 0))); + } catch { + // ignore + } + }; + + const readSkippedStepIds = () => { + try { + const raw = localStorage.getItem(skippedStepsKey); + if (!raw) return new Set(); + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return new Set(); + const ids = parsed + .map((value) => String(value || '').trim()) + .filter(Boolean); + return new Set(ids); + } catch { + return new Set(); + } + }; + + const writeSkippedStepIds = () => { + try { + if (!(state.skippedActionIds instanceof Set) || state.skippedActionIds.size === 0) { + localStorage.removeItem(skippedStepsKey); + return; + } + localStorage.setItem(skippedStepsKey, JSON.stringify(Array.from(state.skippedActionIds))); + } catch { + // ignore + } + }; + + 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 (!Array.isArray(state.actions) || state.actions.length === 0) return false; + const toolsMap = toToolMap(state.diagnostics); + const req = getRequirementState(toolsMap); + if (!req?.coreReady) return true; + return !readCompleted(); + }; + + 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; + } + modal.classList.add('hidden'); + body?.classList?.remove?.('dependency-onboarding-active'); + setBootstrapPending(false); + return true; + }; + const openModal = ({ showWelcome = null } = {}) => { + const wasHidden = modal.classList.contains('hidden'); + modal.classList.remove('hidden'); + 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(); + }; + + 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 [diagRes, actionsRes] = await Promise.all([ + 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 = readSkippedStepIds(); + state.skippedActionIds = new Set( + Array.from(persistedSkippedIds).filter((id) => allowedActionIds.has(id)) + ); + if (state.actions.length > 0) { + const savedStep = readSavedStep(); + setCurrentStep(savedStep, { persist: false }); + } + const view = render(); + applyOnboardingLockUI(); + if (view.req?.coreReady) writeDismissed(false); + + const hasCompletedOnboarding = readCompleted(); + const coreReady = !!view.req?.coreReady; + const shouldAutoShow = isWindowsHost && (!hasCompletedOnboarding || !coreReady) && (forceAutoShow || !readDismissed()); + const shouldKeepVisible = open && !modal.classList.contains('hidden'); + if (explicitOpen || shouldKeepVisible || shouldAutoShow) { + openModal(); + } else { + 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(); + 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 () => { + 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; + } + 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 +10633,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 +10693,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 +10723,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 +14036,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 +14180,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 +14188,7 @@ class ClaudeOrchestrator { window.history.replaceState({}, document.title, window.location.pathname); return tokenFromUrl; } - + // Check localStorage return localStorage.getItem('claude-orchestrator-token'); } @@ -13733,7 +15014,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 +15058,7 @@ class ClaudeOrchestrator { window.__claudeOrchestratorFetchAuthInstalled = true; } - + // Terminal Focus Feature - Now shows only that worktree focusTerminal(sessionId) { // Extract worktree ID from session ID @@ -13815,14 +15096,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 +15115,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 +15157,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 +15200,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 +15247,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 +15273,10 @@ class ClaudeOrchestrator { }, 100); } } - + // Clean up this.focusedTerminalInfo = null; - + // Remove ESC key listener if (this.handleEscKey) { document.removeEventListener('keydown', this.handleEscKey); @@ -14005,14 +15286,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 +15430,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 +15459,7 @@ class ClaudeOrchestrator { } } } - + hideClaudeStartupModal() { const modal = document.getElementById('claude-startup-modal'); if (modal) { @@ -14186,7 +15467,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 +15597,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 +16098,7 @@ class ClaudeOrchestrator { document.addEventListener('keydown', handleEsc); }); } - + updateYoloState(sessionId, checked) { // Update button styles to show YOLO is active const buttons = [ @@ -14825,7 +16106,7 @@ class ClaudeOrchestrator { document.getElementById(`btn-continue-${sessionId}`), document.getElementById(`btn-resume-${sessionId}`) ]; - + buttons.forEach(btn => { if (btn) { if (checked) { @@ -14836,25 +16117,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 +16144,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 +16164,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 +16203,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 +16837,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 +16863,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 +16881,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 +16897,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 +16923,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 +16936,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 +16968,7 @@ class ClaudeOrchestrator { setTimeout(checkAndStart, 500); // Check again in 500ms } }; - + setTimeout(checkAndStart, 1000); // Initial delay for terminal setup } @@ -15696,7 +16977,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 +17083,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 +17116,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 +17124,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 +20960,7 @@ class ClaudeOrchestrator { applyView(); return; } - + state.selectedCardId = card.id || null; applyView(); @@ -21029,7 +22310,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 +22328,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 +22373,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 +31605,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..99b37010 100644 --- a/client/index.html +++ b/client/index.html @@ -1,5 +1,6 @@ + @@ -7,15 +8,17 @@ - + - + +
- +
@@ -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.

+ + + - + - + @@ -1044,10 +1084,12 @@

Notifications

- +
- +
@@ -1062,4 +1104,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..93e88416 100644 --- a/server/diagnosticsService.js +++ b/server/diagnosticsService.js @@ -9,14 +9,34 @@ const execFileAsync = util.promisify(execFile); async function checkCommand(command, args, options = {}) { const timeout = Number(options.timeoutMs) || 2500; try { - const { stdout, stderr } = await execFileAsync(command, args, { + const runOptions = { timeout, windowsHide: true, maxBuffer: 1024 * 1024 - }); + }; + + const commandStr = String(command || '').trim(); + const argsArr = Array.isArray(args) ? args : []; + let result; + try { + result = await execFileAsync(commandStr, argsArr, runOptions); + } catch (error) { + const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr); + const shouldRetryWithCmd = isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT'); + if (!shouldRetryWithCmd) throw error; + result = await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...argsArr], runOptions); + } + + 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 +55,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 +186,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({ diff --git a/server/index.js b/server/index.js index e3a9a62b..0a4e1831 100644 --- a/server/index.js +++ b/server/index.js @@ -99,6 +99,13 @@ 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 { PluginLoaderService } = require('./pluginLoaderService'); const { SchedulerService } = require('./schedulerService'); const { PagerService } = require('./pagerService'); @@ -400,6 +407,7 @@ sessionManager.setGitHelper(gitHelper); // Initialize workspace system let workspaceInitialized = false; +let workspaceSystemReady = null; async function initializeWorkspaceSystem() { try { logger.info('Initializing workspace system...'); @@ -425,21 +433,23 @@ 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 - }); - }) - .catch((error) => { - logger.error('Plugin loader failed', { error: error.message, stack: error.stack }); +workspaceSystemReady = initializeWorkspaceSystem() + .then(() => { + logger.info('Workspace system initialized'); + return true; + }) + .then(() => 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 }); -}).catch(error => { - logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack }); -}); + return true; + }) + .catch(error => { + logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack }); + return false; + }); // WebSocket connection handling io.on('connection', (socket) => { @@ -4050,6 +4060,123 @@ 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.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 +7963,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/setupActionService.js b/server/setupActionService.js new file mode 100644 index 00000000..1bf66f47 --- /dev/null +++ b/server/setupActionService.js @@ -0,0 +1,552 @@ +const crypto = require('crypto'); +const util = require('util'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const { spawn, execFile } = require('child_process'); + +const execFileAsync = util.promisify(execFile); + +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' }; + + const runOptions = { + windowsHide: true, + timeout: 3000, + maxBuffer: 1024 * 1024 + }; + + try { + await execFileAsync(commandStr, Array.isArray(args) ? args : [], runOptions); + return { ok: true }; + } catch (error) { + const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr); + if (isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT')) { + try { + await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...(Array.isArray(args) ? args : [])], runOptions); + return { ok: true }; + } catch (fallbackError) { + return { + ok: false, + error: String(fallbackError?.message || fallbackError || 'Command check failed') + }; + } + } + 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 execFileAsync(command, Array.isArray(args) ? args : [], { + windowsHide: true, + timeout: 9000, + maxBuffer: 1024 * 1024 + }); + return String(result?.stdout || result?.stderr || ''); + } catch (error) { + const stderr = String(error?.stderr || '').trim(); + const stdout = String(error?.stdout || '').trim(); + const message = stderr || stdout || 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'] + } + ); + 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..b9534986 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -42,5 +42,10 @@ "resources/backend/client/*", "resources/backend/node_modules" ] + }, + "plugins": { + "updater": { + "active": false + } } } From b94e71dab502fcf63ac0214b6c475b2f121fc830 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:14:46 +1100 Subject: [PATCH 02/14] fix: include bundled Node.exe in Tauri resource bundle The resources glob missed resources/backend/node/* so node.exe was prepared by the build script but never included in the installer. App would silently exit on launch because it couldn't find Node. Co-Authored-By: Claude Opus 4.6 --- src-tauri/tauri.conf.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b9534986..c2c55ef0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -40,6 +40,7 @@ "resources/backend/*", "resources/backend/server/*", "resources/backend/client/*", + "resources/backend/node/*", "resources/backend/node_modules" ] }, From 889998e0d83d4161a258359d63260cc71657e66d Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:46:05 +1100 Subject: [PATCH 03/14] fix: restore empty updater pubkey (Tauri requires the field) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tauri's updater plugin panics with "missing field pubkey" even when active: false. The empty string is the correct workaround โ€” it satisfies the deserializer while the runtime updater builder (build_runtime_updater) already gates on ORCHESTRATOR_UPDATER_ENABLED env var before attempting any update checks. Co-Authored-By: Claude Opus 4.6 --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c2c55ef0..629f0e47 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -46,7 +46,7 @@ }, "plugins": { "updater": { - "active": false + "pubkey": "" } } } From 26842bec65c026dd501582ba335945346acb34c3 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:02:07 +1100 Subject: [PATCH 04/14] fix: eliminate PowerShell window flashing on Windows startup Replace execFile with spawn + CREATE_NO_WINDOW flag for all diagnostic and setup action commands. On Windows, .cmd/.bat files are routed through cmd.exe directly (avoiding retry path that flashed windows). The creationFlags: 0x08000000 flag prevents any console window from appearing. Co-Authored-By: Claude Opus 4.6 --- server/diagnosticsService.js | 70 +++++++++++++------- server/setupActionService.js | 80 +++++++++++++---------- tests/unit/diagnosticsService.test.js | 92 +++++++++++++++++++++------ 3 files changed, 166 insertions(+), 76 deletions(-) diff --git a/server/diagnosticsService.js b/server/diagnosticsService.js index 93e88416..3761cc3e 100644 --- a/server/diagnosticsService.js +++ b/server/diagnosticsService.js @@ -1,31 +1,55 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); -const util = require('util'); -const { execFile } = require('child_process'); +const { spawn } = require('child_process'); -const execFileAsync = util.promisify(execFile); +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 runOptions = { - timeout, - windowsHide: true, - maxBuffer: 1024 * 1024 - }; - - const commandStr = String(command || '').trim(); - const argsArr = Array.isArray(args) ? args : []; - let result; - try { - result = await execFileAsync(commandStr, argsArr, runOptions); - } catch (error) { - const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr); - const shouldRetryWithCmd = isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT'); - if (!shouldRetryWithCmd) throw error; - result = await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...argsArr], runOptions); - } + const result = await execQuiet(command, args, { timeout, maxBuffer: 1024 * 1024 }); const { stdout, stderr } = result || {}; const output = String(stdout || stderr || '').trim(); @@ -749,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/setupActionService.js b/server/setupActionService.js index 1bf66f47..4ad5e234 100644 --- a/server/setupActionService.js +++ b/server/setupActionService.js @@ -1,11 +1,50 @@ const crypto = require('crypto'); -const util = require('util'); const path = require('path'); const fs = require('fs'); const os = require('os'); -const { spawn, 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) || 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(); @@ -66,28 +105,10 @@ async function checkExecutable(command, args = ['--version']) { const commandStr = String(command || '').trim(); if (!commandStr) return { ok: false, error: 'Missing command' }; - const runOptions = { - windowsHide: true, - timeout: 3000, - maxBuffer: 1024 * 1024 - }; - try { - await execFileAsync(commandStr, Array.isArray(args) ? args : [], runOptions); + await execQuiet(commandStr, Array.isArray(args) ? args : [], { timeout: 3000 }); return { ok: true }; } catch (error) { - const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr); - if (isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT')) { - try { - await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...(Array.isArray(args) ? args : [])], runOptions); - return { ok: true }; - } catch (fallbackError) { - return { - ok: false, - error: String(fallbackError?.message || fallbackError || 'Command check failed') - }; - } - } return { ok: false, error: String(error?.message || error || 'Command check failed') @@ -122,16 +143,10 @@ async function resolveGitCommand(platform = process.platform) { async function runGitCommand(command, args = []) { try { - const result = await execFileAsync(command, Array.isArray(args) ? args : [], { - windowsHide: true, - timeout: 9000, - maxBuffer: 1024 * 1024 - }); + const result = await execQuiet(command, Array.isArray(args) ? args : [], { timeout: 9000 }); return String(result?.stdout || result?.stderr || ''); } catch (error) { - const stderr = String(error?.stderr || '').trim(); - const stdout = String(error?.stdout || '').trim(); - const message = stderr || stdout || String(error?.message || error || 'Git command failed'); + const message = String(error?.message || error || 'Git command failed'); const err = new Error(message); err.code = String(error?.code || 'git_command_failed'); throw err; @@ -373,7 +388,8 @@ function launchPowerShellCommand(action) { { detached: false, windowsHide: true, - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['ignore', 'pipe', 'pipe'], + ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {}) } ); run.pid = Number.isFinite(child?.pid) ? child.pid : null; 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, '', ''); From 342692c5686f8bc5da47f1d9cc1244c7357ac016 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:16:05 +1100 Subject: [PATCH 05/14] fix: stop all PowerShell window flashing on Windows Root cause: sessionManager.checkProcessLimit() spawns powershell.exe every 5 seconds per terminal session using execFile without CREATE_NO_WINDOW flag. With 4 sessions = 4 visible PowerShell windows every 5 seconds. Fixed all spawn points across 4 files: - sessionManager.js: checkProcessLimit() and taskkill - index.js: getChildPids() and getRssKb() in /api/process/performance - diagnosticsService.js: all diagnostic checks (already fixed) - setupActionService.js: all setup action spawns (already fixed) All now use spawn() with creationFlags: 0x08000000 (CREATE_NO_WINDOW) instead of execFile(). Co-Authored-By: Claude Opus 4.6 --- server/index.js | 31 +++++++++++++++++---------- server/sessionManager.js | 46 +++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/server/index.js b/server/index.js index 0a4e1831..84cdde19 100644 --- a/server/index.js +++ b/server/index.js @@ -2313,9 +2313,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) => { @@ -2323,15 +2321,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+/) @@ -2339,7 +2349,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)) @@ -2354,17 +2364,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; diff --git a/server/sessionManager.js b/server/sessionManager.js index d439f289..5346ef8c 100644 --- a/server/sessionManager.js +++ b/server/sessionManager.js @@ -2357,15 +2357,24 @@ 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('close', () => { + clearTimeout(timer); const processCount = parseInt(String(stdout || '').trim(), 10); if (!Number.isFinite(processCount)) return; if (processCount > this.maxProcessesPerSession) { @@ -2381,10 +2390,17 @@ 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('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 +2408,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 +2484,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; } From 56274a002762c0345b7638ed0b4f326dca61edcc Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:26:47 +1100 Subject: [PATCH 06/14] fix: stop onboarding wizard from re-appearing after completion The shouldAutoShow logic included `|| !coreReady` which forced the wizard to re-open if Claude/Codex CLI wasn't in PATH, even after the user had already completed onboarding. Now once completed, it stays completed. Co-Authored-By: Claude Opus 4.6 --- client/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app.js b/client/app.js index 6327b8ba..72604bdc 100644 --- a/client/app.js +++ b/client/app.js @@ -10041,7 +10041,7 @@ class ClaudeOrchestrator { const hasCompletedOnboarding = readCompleted(); const coreReady = !!view.req?.coreReady; - const shouldAutoShow = isWindowsHost && (!hasCompletedOnboarding || !coreReady) && (forceAutoShow || !readDismissed()); + const shouldAutoShow = isWindowsHost && !hasCompletedOnboarding && (forceAutoShow || !readDismissed()); const shouldKeepVisible = open && !modal.classList.contains('hidden'); if (explicitOpen || shouldKeepVisible || shouldAutoShow) { openModal(); From 294956a691994a322c2b9ecc07d22066291c0fdc Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:23:39 +1100 Subject: [PATCH 07/14] fix: unlock onboarding modal once user has completed it isOnboardingLocked() was requiring coreReady (git + agent CLI) to unlock. If Claude/Codex CLI wasn't in PATH, the modal was permanently locked open and couldn't be dismissed. Now once readCompleted() is true, the lock is released regardless of coreReady state. Co-Authored-By: Claude Opus 4.6 --- client/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/app.js b/client/app.js index 72604bdc..dc79a5fa 100644 --- a/client/app.js +++ b/client/app.js @@ -9658,11 +9658,11 @@ class ClaudeOrchestrator { 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); - if (!req?.coreReady) return true; - return !readCompleted(); + return !req?.coreReady; }; const applyOnboardingLockUI = () => { From cb10d54af3142f795d1a563045961eabd57d1f74 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:31:12 +1100 Subject: [PATCH 08/14] fix: skip onboarding boot screen when already completed The dependency-onboarding-booting CSS class (which hides all page content) was applied unconditionally on Windows hosts before any async check. Now it reads the completion flag from localStorage first and skips the boot overlay if onboarding was already completed. Co-Authored-By: Claude Opus 4.6 --- client/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/app.js b/client/app.js index dc79a5fa..0f844c40 100644 --- a/client/app.js +++ b/client/app.js @@ -9355,12 +9355,14 @@ class ClaudeOrchestrator { } body?.classList?.remove?.('dependency-onboarding-booting'); }; - setBootstrapPending(true); if (!isWindowsHost) { setBootstrapPending(false); return; } + const completedEarly = (() => { try { return localStorage.getItem(completedKey) === 'true'; } catch { return false; } })(); + setBootstrapPending(!completedEarly); + const dismissKey = 'orchestrator-dependency-setup-dismissed-v3'; const completedKey = 'orchestrator-dependency-onboarding-completed-v2'; const progressKey = 'orchestrator-dependency-onboarding-progress-v2'; From 771cb75cb8673e9a35ae8135ababfcd0e00abc39 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:17:55 +1100 Subject: [PATCH 09/14] feat: persist onboarding state in app data --- CODEBASE_DOCUMENTATION.md | 6 +- server/index.js | 23 +++++ server/onboardingStateService.js | 120 ++++++++++++++++++++++ tests/unit/onboardingStateService.test.js | 72 +++++++++++++ 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 server/onboardingStateService.js create mode 100644 tests/unit/onboardingStateService.test.js diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index 41397f7b..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 @@ -543,7 +546,8 @@ LOGGING: Winston-based structured logging with rotation ``` server/setupActionService.js - Defines setup actions and launches PowerShell installers -server/index.js - Routes: GET /api/setup-actions, POST /api/setup-actions/run +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 diff --git a/server/index.js b/server/index.js index 84cdde19..bb0e4f22 100644 --- a/server/index.js +++ b/server/index.js @@ -106,6 +106,7 @@ const { getLatestSetupActionRun, configureGitIdentity } = require('./setupActionService'); +const { OnboardingStateService } = require('./onboardingStateService'); const { PluginLoaderService } = require('./pluginLoaderService'); const { SchedulerService } = require('./schedulerService'); const { PagerService } = require('./pagerService'); @@ -302,6 +303,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 }); @@ -4081,6 +4083,27 @@ app.get('/api/setup-actions', (req, res) => { } }); +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(); 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/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'] + }); + }); +}); From 7fd3dbed694b6b06457810b32d9e2d46e8732521 Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:18:08 +1100 Subject: [PATCH 10/14] fix: persist onboarding progress across restarts --- client/app.js | 245 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 175 insertions(+), 70 deletions(-) diff --git a/client/app.js b/client/app.js index 0f844c40..1fb95daa 100644 --- a/client/app.js +++ b/client/app.js @@ -9360,13 +9360,149 @@ class ClaudeOrchestrator { return; } - const completedEarly = (() => { try { return localStorage.getItem(completedKey) === 'true'; } catch { return false; } })(); - setBootstrapPending(!completedEarly); - 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 persistedSetupState = { + loaded: false, + loadPromise: null, + savePromise: Promise.resolve(), + current: normalizeSetupState({ + dismissed: readLegacyBool(dismissKey), + completed: readLegacyBool(completedKey), + currentStep: readLegacyStep(), + skippedActionIds: readLegacySkippedActionIds() + }) + }; + 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; + }; + const completedEarly = getPersistedSetupState().completed; + setBootstrapPending(!completedEarly); + const state = { loading: false, diagnostics: null, @@ -9383,83 +9519,39 @@ class ClaudeOrchestrator { gitIdentityHelpVisible: false }; - const readDismissed = () => { - try { - return localStorage.getItem(dismissKey) === 'true'; - } catch { - return false; - } - }; + const readDismissed = () => getPersistedSetupState().dismissed === true; const writeDismissed = (value) => { - try { - if (value) localStorage.setItem(dismissKey, 'true'); - else localStorage.removeItem(dismissKey); - } catch { - // ignore - } + const next = value === true; + if (readDismissed() === next) return; + void savePersistedSetupState({ dismissed: next }); }; - const readCompleted = () => { - try { - return localStorage.getItem(completedKey) === 'true'; - } catch { - return false; - } - }; + const readCompleted = () => getPersistedSetupState().completed === true; const writeCompleted = (value) => { - try { - if (value) localStorage.setItem(completedKey, 'true'); - else localStorage.removeItem(completedKey); - } catch { - // ignore - } + const next = value === true; + if (readCompleted() === next) return; + void savePersistedSetupState({ completed: next }); }; - const readSavedStep = () => { - try { - const raw = Number.parseInt(String(localStorage.getItem(progressKey) || ''), 10); - if (Number.isFinite(raw) && raw >= 0) return raw; - return 0; - } catch { - return 0; - } - }; + const readSavedStep = () => Math.max(0, Number(getPersistedSetupState().currentStep) || 0); const writeSavedStep = (step) => { - try { - localStorage.setItem(progressKey, String(Math.max(0, Number(step) || 0))); - } catch { - // ignore - } + const next = Math.max(0, Number(step) || 0); + if (readSavedStep() === next) return; + void savePersistedSetupState({ currentStep: next }); }; - const readSkippedStepIds = () => { - try { - const raw = localStorage.getItem(skippedStepsKey); - if (!raw) return new Set(); - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) return new Set(); - const ids = parsed - .map((value) => String(value || '').trim()) - .filter(Boolean); - return new Set(ids); - } catch { - return new Set(); - } - }; + const readSkippedStepIds = () => new Set(getPersistedSetupState().skippedActionIds); const writeSkippedStepIds = () => { - try { - if (!(state.skippedActionIds instanceof Set) || state.skippedActionIds.size === 0) { - localStorage.removeItem(skippedStepsKey); - return; - } - localStorage.setItem(skippedStepsKey, JSON.stringify(Array.from(state.skippedActionIds))); - } catch { - // ignore - } + 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) => { @@ -9970,6 +10062,9 @@ class ClaudeOrchestrator { openModal(); return false; } + if (!force && !readCompleted()) { + writeDismissed(true); + } modal.classList.add('hidden'); body?.classList?.remove?.('dependency-onboarding-active'); setBootstrapPending(false); @@ -10005,7 +10100,8 @@ class ClaudeOrchestrator { if (state.loading) return false; setLoading(true); try { - const [diagRes, actionsRes] = await Promise.all([ + const [persisted, diagRes, actionsRes] = await Promise.all([ + loadPersistedSetupState(), fetch('/api/diagnostics'), fetch('/api/setup-actions') ]); @@ -10029,12 +10125,15 @@ class ClaudeOrchestrator { .map((action) => String(action?.id || '').trim()) .filter(Boolean) ); - const persistedSkippedIds = readSkippedStepIds(); + const persistedSkippedIds = new Set( + (Array.isArray(persisted?.skippedActionIds) ? persisted.skippedActionIds : []) + .filter((id) => allowedActionIds.has(String(id || '').trim())) + ); state.skippedActionIds = new Set( - Array.from(persistedSkippedIds).filter((id) => allowedActionIds.has(id)) + Array.from(persistedSkippedIds) ); if (state.actions.length > 0) { - const savedStep = readSavedStep(); + const savedStep = Math.max(0, Number(persisted?.currentStep) || 0); setCurrentStep(savedStep, { persist: false }); } const view = render(); @@ -10278,6 +10377,12 @@ class ClaudeOrchestrator { }; const runBootstrapLoad = async () => { + try { + const persisted = await loadPersistedSetupState(); + setBootstrapPending(!(persisted?.completed === true)); + } catch { + // ignore + } const delaysMs = [0, 240, 420, 700, 1050, 1450, 1900]; for (let attempt = 0; attempt < delaysMs.length; attempt += 1) { if (attempt > 0) { From 739f0ec5223687923363a565c953b3840bf0badf Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:54:05 +1100 Subject: [PATCH 11/14] fix: prevent onboarding startup flicker --- client/app.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/client/app.js b/client/app.js index 1fb95daa..16f96bc4 100644 --- a/client/app.js +++ b/client/app.js @@ -9500,8 +9500,7 @@ class ClaudeOrchestrator { }); return persistedSetupState.savePromise; }; - const completedEarly = getPersistedSetupState().completed; - setBootstrapPending(!completedEarly); + setBootstrapPending(true); const state = { loading: false, @@ -9516,7 +9515,8 @@ class ClaudeOrchestrator { name: '', email: '' }, - gitIdentityHelpVisible: false + gitIdentityHelpVisible: false, + startupPending: true }; const readDismissed = () => getPersistedSetupState().dismissed === true; @@ -10070,9 +10070,17 @@ class ClaudeOrchestrator { setBootstrapPending(false); return true; }; - const openModal = ({ showWelcome = null } = {}) => { + 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') { @@ -10084,6 +10092,7 @@ class ClaudeOrchestrator { render(); } applyOnboardingLockUI(); + return true; }; const setLoading = (loading) => { @@ -10142,11 +10151,16 @@ class ClaudeOrchestrator { const hasCompletedOnboarding = readCompleted(); const coreReady = !!view.req?.coreReady; + if (hasCompletedOnboarding) { + state.startupPending = false; + closeModal({ force: true }); + } const shouldAutoShow = isWindowsHost && !hasCompletedOnboarding && (forceAutoShow || !readDismissed()); - const shouldKeepVisible = open && !modal.classList.contains('hidden'); + const shouldKeepVisible = !hasCompletedOnboarding && open && !modal.classList.contains('hidden'); if (explicitOpen || shouldKeepVisible || shouldAutoShow) { - openModal(); + openModal({ allowDuringStartup: bootstrap || explicitOpen }); } else { + state.startupPending = false; setBootstrapPending(false); } return true; @@ -10154,7 +10168,7 @@ class ClaudeOrchestrator { 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(); + if (shouldOpenOnError) openModal({ allowDuringStartup: bootstrap || explicitOpen }); else if (!bootstrap) setBootstrapPending(false); return false; } finally { @@ -10377,12 +10391,8 @@ class ClaudeOrchestrator { }; const runBootstrapLoad = async () => { - try { - const persisted = await loadPersistedSetupState(); - setBootstrapPending(!(persisted?.completed === true)); - } catch { - // ignore - } + 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) { @@ -10391,6 +10401,7 @@ class ClaudeOrchestrator { const ok = await loadAndRender({ open: false, forceAutoShow: false, bootstrap: true }); if (ok) return; } + state.startupPending = false; setBootstrapPending(false); }; From 4bc626a785a09afe13c106abd37b0a278ad2646b Mon Sep 17 00:00:00 2001 From: web3dev1337 <160291380+web3dev1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:28:46 +1100 Subject: [PATCH 12/14] fix: prevent onboarding flash before bootstrap --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 99b37010..f61cc478 100644 --- a/client/index.html +++ b/client/index.html @@ -19,7 +19,7 @@ href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="> - +