diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md
index eadc9144..63d3be44 100644
--- a/CODEBASE_DOCUMENTATION.md
+++ b/CODEBASE_DOCUMENTATION.md
@@ -465,6 +465,9 @@ POST /api/discord/process-queue - Dispatch queue processing prompt with optio
POST /api/sessions/intent-haiku - Generate <=200 char intent summary for an active Claude/Codex session
GET /api/greenfield/categories - Greenfield category list (taxonomy-backed)
POST /api/greenfield/detect-category - Infer category from description (taxonomy keyword matching)
+GET /api/setup-actions - List Windows dependency-onboarding actions
+GET /api/setup-actions/state - Read persisted dependency-onboarding state (completed/dismissed/current step)
+PUT /api/setup-actions/state - Persist dependency-onboarding state into app data for desktop restarts
GET /api/user-settings - Get user preferences
PUT /api/user-settings - Update user preferences
@@ -538,5 +541,17 @@ LOGGING: Winston-based structured logging with rotation
9. **Mixed-repo workspaces**: Terminal naming must avoid conflicts between repos
10. **Template validation**: Always validate workspace templates against schemas
+
+## First-Run Dependency Onboarding (Windows)
+
+```
+server/setupActionService.js - Defines setup actions and launches PowerShell installers
+server/onboardingStateService.js - Persists Windows dependency-onboarding state in app data so Tauri restarts survive per-launch localhost ports
+server/index.js - Routes: GET/PUT /api/setup-actions/state plus setup action execution endpoints
+client/app.js - Guided dependency onboarding steps + diagnostics integration
+client/index.html - Dependency onboarding modal markup + launch button
+client/styles.css - Dependency onboarding progress/step styling
+```
+
---
๐จ **END OF FILE - ENSURE YOU READ EVERYTHING ABOVE** ๐จ
diff --git a/client/app.js b/client/app.js
index c9936ba9..9a0cccdb 100644
--- a/client/app.js
+++ b/client/app.js
@@ -845,7 +845,7 @@ class ClaudeOrchestrator {
if (!expiresAt || expiresAt <= now) this.pendingWorktreeReservations.delete(key);
}
}
-
+
async init() {
try {
// Initialize managers
@@ -984,21 +984,21 @@ class ClaudeOrchestrator {
if (this.settings.notifications) {
this.notificationManager.requestPermission();
}
-
+
// Set up UI
this.setupEventListeners();
this.applyTheme();
this.syncSettingsUI();
this.applySimpleModeConfig();
this.installAuthFetchShim();
-
+
// Connect to server
await this.connectToServer();
await this.ensureProjectTypeTaxonomy();
// Hook panels that depend on socket events
this.activityFeedPanel?.onSocketConnected?.(this.socket);
-
+
// Load user settings from server
await this.loadUserSettings();
this.applySidebarDesktopCollapsedFromPrefs();
@@ -1016,22 +1016,22 @@ class ClaudeOrchestrator {
// WIP / Queue banner (process status)
this.startProcessStatusBanner();
-
+
// Check for updates on startup
this.checkForSettingsUpdates();
-
+
// Hide loading message if it exists
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.classList.add('hidden');
}
-
+
} catch (error) {
console.error('Failed to initialize:', error);
this.showError('Failed to initialize application');
}
}
-
+
async connectToServer() {
return new Promise((resolve, reject) => {
console.log('Attempting to connect to server...');
@@ -1044,29 +1044,29 @@ class ClaudeOrchestrator {
const serverUrl = window.location.origin;
this.socket = io(serverUrl, socketOptions);
console.log(`Socket connecting to ${serverUrl}...`);
-
+
// Connection events
this.socket.on('connect', () => {
console.log('Connected to server');
this.updateConnectionStatus(true);
resolve();
});
-
+
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.updateConnectionStatus(false);
-
+
if (error.message === 'Authentication failed') {
this.showError('Authentication failed. Please check your token.');
}
reject(error);
});
-
+
this.socket.on('disconnect', () => {
console.log('Disconnected from server');
this.updateConnectionStatus(false);
});
-
+
// Session events
this.socket.on('sessions', async (sessionStates) => {
console.log('Received sessions event:', sessionStates);
@@ -1085,7 +1085,7 @@ class ClaudeOrchestrator {
this.worktreeTags.set(worktreePath, tag || {});
this.buildSidebar();
});
-
+
this.socket.on('terminal-output', ({ sessionId, data }) => {
this.terminalManager.handleOutput(sessionId, data);
@@ -1133,7 +1133,7 @@ class ClaudeOrchestrator {
this.sessionActivity.set(sessionId, 'active');
}
});
-
+
this.socket.on('autosuggest-response', ({ sessionId, suggestion, prefix }) => {
this.terminalManager.handleAutosuggestResponse(sessionId, suggestion, prefix);
});
@@ -1142,15 +1142,15 @@ class ClaudeOrchestrator {
this.updateSessionStatus(sessionId, status);
this.maybeAutoSendPrompt(sessionId, status);
});
-
+
this.socket.on('branch-update', ({ sessionId, branch, remoteUrl, defaultBranch, existingPR }) => {
this.updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR);
});
-
+
this.socket.on('notification-trigger', (notification) => {
this.notificationManager.handleNotification(notification);
});
-
+
this.socket.on('session-exited', ({ sessionId, exitCode }) => {
this.handleSessionExit(sessionId, exitCode);
});
@@ -1343,7 +1343,7 @@ class ClaudeOrchestrator {
// Hide + persist dismissal so it doesn't resurrect on refresh/worktree-add
this.hideStartupUI(sessionId);
this.scheduleAutoPromptFallback(sessionId, 'claude');
-
+
// Enable the start button now that Claude has started
const startBtn = document.getElementById(`claude-start-btn-${sessionId}`);
if (startBtn) {
@@ -1361,7 +1361,7 @@ class ClaudeOrchestrator {
this.socket.on('claude-update-required', (updateInfo) => {
this.showClaudeUpdateRequired(updateInfo);
});
-
+
this.socket.on('user-settings-updated', (settings) => {
console.log('User settings updated:', settings);
this.userSettings = settings;
@@ -1589,7 +1589,7 @@ class ClaudeOrchestrator {
this.socket.on('git-updated', (result) => {
console.log('Git updated:', result);
this.showTemporaryMessage(`Repository updated successfully! ${result.wasUpToDate ? 'Already up to date.' : 'Changes pulled.'}`, 'success');
-
+
// Refresh the page after successful update
if (!result.wasUpToDate) {
setTimeout(() => {
@@ -1600,45 +1600,45 @@ class ClaudeOrchestrator {
}, 3000);
}
});
-
+
// Build production events
this.socket.on('build-started', ({ sessionId, worktreeNum }) => {
console.log(`Build started for worktree ${worktreeNum}`);
});
-
+
this.socket.on('build-completed', ({ sessionId, worktreeNum, zipPath }) => {
console.log(`Build completed for worktree ${worktreeNum}: ${zipPath}`);
-
+
// Restore the build button (use work{num} pattern to find buttons)
this.restoreBuildButton(`work${worktreeNum}`);
-
+
// Request to reveal the file in explorer
this.socket.emit('reveal-in-explorer', { path: zipPath });
});
-
+
this.socket.on('build-failed', ({ sessionId, worktreeNum, error }) => {
console.error(`Build failed for worktree ${worktreeNum}:`, error);
this.showError(`โ Build failed for Worktree ${worktreeNum}: ${error}`);
-
+
// Restore the build button (use work{num} pattern to find buttons)
this.restoreBuildButton(`work${worktreeNum}`);
});
-
+
// Periodic heartbeat to keep sessions alive while UI is open
this.startHeartbeats();
-
+
this.socket.on('server-started', ({ sessionId, port }) => {
console.log(`[SERVER-STARTED EVENT] Session: ${sessionId}, Port: ${port}`);
this.serverPorts.set(sessionId, port);
console.log(`Server ${sessionId} started on port ${port}`);
console.log('Current serverPorts:', Array.from(this.serverPorts.entries()));
-
+
// Only open localhost automatically - Hytopia needs manual click due to popup blockers
setTimeout(() => {
const localhostUrl = `https://localhost:${port}`;
console.log(`Opening localhost for initialization: ${localhostUrl}`);
window.open(localhostUrl, '_blank');
-
+
// Show notification that server is ready
if (this.settings.notifications) {
this.showNotification('Server Ready', `Server ${sessionId.replace('-server', '')} is running on port ${port}. Click ๐ฎ to play!`);
@@ -1657,14 +1657,14 @@ class ClaudeOrchestrator {
reject(new Error('Connection timeout'));
}
}, 10000);
-
+
// Clear timeout on successful connection
this.socket.on('connect', () => {
clearTimeout(timeoutId);
});
});
}
-
+
startHeartbeats() {
if (this._heartbeatInterval) {
clearInterval(this._heartbeatInterval);
@@ -1676,7 +1676,7 @@ class ClaudeOrchestrator {
}
}, 30000);
}
-
+
setupEventListeners() {
// Check if elements exist before adding listeners
const elements = {
@@ -1739,7 +1739,7 @@ class ClaudeOrchestrator {
'start-claude',
'cancel-claude-startup'
]);
-
+
// Check all elements exist
for (const id in elements) {
elements[id] = document.getElementById(id);
@@ -1747,7 +1747,7 @@ class ClaudeOrchestrator {
console.warn(`Element not found: ${id}`);
}
}
-
+
// Sidebar worktree clicks - use toggle instead of show
if (elements['worktree-list']) {
elements['worktree-list'].addEventListener('click', (e) => {
@@ -1856,7 +1856,7 @@ class ClaudeOrchestrator {
if (!document.body.classList.contains('sidebar-open')) return;
this.closeSidebar();
});
-
+
// View buttons
const dashboardBtn = document.getElementById('dashboard-btn');
if (dashboardBtn) {
@@ -1878,26 +1878,26 @@ class ClaudeOrchestrator {
this.setViewMode('all');
if (this.isMobileLayout()) this.closeSidebar();
});
-
+
document.getElementById('view-claude-only').addEventListener('click', () => {
this.setViewMode('claude');
if (this.isMobileLayout()) this.closeSidebar();
});
-
+
document.getElementById('view-servers-only').addEventListener('click', () => {
this.setViewMode('server');
if (this.isMobileLayout()) this.closeSidebar();
});
-
+
// Presets
document.getElementById('view-presets').addEventListener('click', () => {
document.getElementById('presets-modal').classList.remove('hidden');
});
-
+
document.getElementById('close-presets').addEventListener('click', () => {
document.getElementById('presets-modal').classList.add('hidden');
});
-
+
// Preset buttons
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -1906,9 +1906,9 @@ class ClaudeOrchestrator {
document.getElementById('presets-modal').classList.add('hidden');
});
});
-
+
// Grid layout dropdown removed - using dynamic layout now
-
+
// Settings
const settingsToggle = document.getElementById('settings-toggle');
const closeSettingsPanel = () => {
@@ -1936,7 +1936,7 @@ class ClaudeOrchestrator {
} else {
console.error('Settings toggle button not found!');
}
-
+
document.getElementById('close-settings').addEventListener('click', () => {
closeSettingsPanel();
});
@@ -1962,7 +1962,7 @@ class ClaudeOrchestrator {
if (panel.contains(e.target)) return;
closeSettingsPanel();
});
-
+
// Settings inputs
document.getElementById('enable-notifications').addEventListener('change', (e) => {
this.settings.notifications = e.target.checked;
@@ -1971,12 +1971,12 @@ class ClaudeOrchestrator {
this.notificationManager.requestPermission();
}
});
-
+
document.getElementById('enable-sounds').addEventListener('change', (e) => {
this.settings.sounds = e.target.checked;
this.saveSettings();
});
-
+
document.getElementById('auto-scroll').addEventListener('change', (e) => {
this.settings.autoScroll = e.target.checked;
this.saveSettings();
@@ -1994,7 +1994,7 @@ class ClaudeOrchestrator {
}
}
});
-
+
document.getElementById('theme-select').addEventListener('change', (e) => {
this.settings.theme = e.target.value;
this.saveSettings();
@@ -2048,6 +2048,7 @@ class ClaudeOrchestrator {
// Settings UI helpers: search + section jump so the panel doesnโt feel like an endless scroll.
this.setupSettingsPanelNavigation();
this.setupDiagnosticsPanel();
+ this.setupDependencySetupWizard();
const tasksThemeSelect = document.getElementById('tasks-theme-select');
if (tasksThemeSelect) {
@@ -2306,7 +2307,7 @@ class ClaudeOrchestrator {
document.getElementById('dismiss-git-notification').addEventListener('click', () => {
document.getElementById('git-update-notification').classList.add('hidden');
});
-
+
// Workflow notification settings (server-persisted)
const workflowNotifyMode = document.getElementById('workflow-notify-mode');
if (workflowNotifyMode) {
@@ -2727,33 +2728,33 @@ class ClaudeOrchestrator {
if (markReadBtn) {
markReadBtn.addEventListener('click', () => this.notificationManager?.markAllAsRead?.());
}
-
+
// Claude startup modal handlers (simplified)
const cancelClaudeBtn = document.getElementById('cancel-claude-startup');
-
+
if (cancelClaudeBtn) {
cancelClaudeBtn.addEventListener('click', () => {
this.hideClaudeStartupModal();
});
}
-
+
// Handle startup option button clicks
document.addEventListener('click', (e) => {
if (e.target.closest('.startup-option-btn')) {
const btn = e.target.closest('.startup-option-btn');
const mode = btn.dataset.mode;
-
+
// Check if modal YOLO is checked
const modalYolo = document.getElementById('modal-yolo');
const skipPermissions = modalYolo ? modalYolo.checked : false;
-
+
if (this.pendingClaudeSession) {
this.startClaudeWithOptions(this.pendingClaudeSession, mode, skipPermissions);
this.hideClaudeStartupModal();
}
}
});
-
+
// Handle window resize to fix blank terminals
let resizeTimeout;
window.addEventListener('resize', () => {
@@ -2899,7 +2900,7 @@ class ClaudeOrchestrator {
});
});
}
-
+
setViewMode(mode, { persist = true } = {}) {
const normalized = String(mode || '').toLowerCase();
if (!['all', 'claude', 'server'].includes(normalized)) return;
@@ -2914,7 +2915,7 @@ class ClaudeOrchestrator {
this.updateGlobalUserSetting('ui.terminals.viewMode', normalized);
}
}
-
+
updateViewModeButtons() {
const allBtn = document.getElementById('view-all');
const claudeBtn = document.getElementById('view-claude-only');
@@ -3544,21 +3545,21 @@ class ClaudeOrchestrator {
return null;
}
-
+
matchesViewMode(sessionId) {
if (this.viewMode === 'all') return true;
-
+
const session = this.sessions.get(sessionId);
const type = session?.type;
-
+
if (this.viewMode === 'claude') {
return type === 'claude' || type === 'codex' || /-(claude|codex)$/.test(String(sessionId || ''));
}
-
+
if (this.viewMode === 'server') {
return type === 'server' || sessionId.includes('-server');
}
-
+
return true;
}
@@ -3571,7 +3572,7 @@ class ClaudeOrchestrator {
&& this.matchesTierFilter(sessionId)
&& this.matchesWorkflowMode(sessionId);
}
-
+
handleInitialSessions(sessionStates) {
console.log('Received initial sessions:', sessionStates);
@@ -3588,7 +3589,7 @@ class ClaudeOrchestrator {
this.sessions.clear();
this.sessionActivity.clear();
this.visibleTerminals.clear();
-
+
// Process sessions
for (const [sessionId, state] of Object.entries(sessionStates)) {
const sessionData = {
@@ -3634,13 +3635,13 @@ class ClaudeOrchestrator {
this.pruneIntentHaikuState(new Set(Object.keys(sessionStates)));
this.lastSessionsWorkspaceId = currentWorkspaceId;
-
+
// Hide loading message FIRST
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.style.display = 'none';
}
-
+
// Build sidebar
this.buildSidebar();
@@ -4237,10 +4238,10 @@ class ClaudeOrchestrator {
// Always ensure filter toggle exists and is updated FIRST
this.ensureFilterToggleExists();
-
+
// Clear and rebuild the worktree list
worktreeList.innerHTML = '';
-
+
// Group sessions by worktree and repository for mixed-repo support
const worktrees = new Map();
@@ -4274,22 +4275,22 @@ class ClaudeOrchestrator {
worktree.server = session;
}
}
-
+
// Create sidebar items
for (const [worktreeId, worktree] of worktrees) {
// Check if worktree is active (has any session marked as active)
const isActive = this.isWorktreeActive(worktreeId);
-
+
// Skip inactive worktrees if filter is enabled
if (this.showActiveOnly && !isActive) {
continue;
}
-
+
// Check if any session in this worktree is visible.
const claudeVisible = !!(worktree.claude && this.isSessionVisibleByWorktreeSelection(worktree.claude.sessionId, worktree.claude));
const serverVisible = !!(worktree.server && this.isSessionVisibleByWorktreeSelection(worktree.server.sessionId, worktree.server));
const isVisible = claudeVisible || serverVisible;
-
+
const item = document.createElement('div');
// Only show visibility state, not activity state (activity filtering is handled separately)
item.className = `worktree-item ${!isVisible ? 'hidden-terminal' : ''}`;
@@ -4408,9 +4409,9 @@ class ClaudeOrchestrator {
`;
-
+
// Click handler is already attached via event delegation in setupEventListeners
-
+
worktreeList.appendChild(item);
}
@@ -4791,21 +4792,21 @@ class ClaudeOrchestrator {
const current = this.worktreeTags.get(worktreePath)?.readyForReview;
return this.setWorktreeReadyForReview(worktreePath, !current);
}
-
+
ensureFilterToggleExists() {
let filterToggle = document.getElementById('filter-toggle');
-
+
if (!filterToggle) {
// Create the filter toggle element
filterToggle = document.createElement('div');
filterToggle.className = 'filter-toggle';
filterToggle.id = 'filter-toggle';
-
+
// Insert it right before the worktree list
const worktreeList = document.getElementById('worktree-list');
worktreeList.parentNode.insertBefore(filterToggle, worktreeList);
}
-
+
// Always update the button content
const visibility = this.getUiVisibilityConfig().sidebar || {};
const showActiveFilter = visibility.activeFilter !== false;
@@ -4838,7 +4839,7 @@ class ClaudeOrchestrator {
if (status === 'error') return 'error';
return 'idle';
}
-
+
isWorktreeActive(worktreeIdOrKey) {
// Check if any session for this worktree has been marked as active.
// For mixed-repo workspaces we may receive:
@@ -4867,11 +4868,11 @@ class ClaudeOrchestrator {
return false;
}
-
+
toggleActivityFilter() {
this.showActiveOnly = !this.showActiveOnly;
this.buildSidebar();
-
+
// Also update the main grid view to match the filter
if (this.showActiveOnly) {
this.showActiveWorktreesOnly();
@@ -4879,11 +4880,11 @@ class ClaudeOrchestrator {
this.showAllTerminals();
}
}
-
+
showActiveWorktreesOnly() {
// Clear visible terminals first
this.visibleTerminals.clear();
-
+
// Add only active worktree sessions to visible set
for (const [sessionId, session] of this.sessions) {
const sessionWorktreeId = session.worktreeId || sessionId.split('-')[0];
@@ -4894,7 +4895,7 @@ class ClaudeOrchestrator {
this.visibleTerminals.add(sessionId);
}
}
-
+
// If no active sessions, show all
if (this.visibleTerminals.size === 0) {
this.showAllTerminals();
@@ -4903,7 +4904,7 @@ class ClaudeOrchestrator {
this.buildSidebar();
}
}
-
+
resizeAllVisibleTerminals() {
// Force resize all visible terminals to fit their containers
this.activeView.forEach(sessionId => {
@@ -5037,7 +5038,7 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
showWorktree(worktreeIdOrKey) {
// Show terminals for this EXACT worktree key
const claudeId = `${worktreeIdOrKey}-claude`;
@@ -5049,17 +5050,17 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
showAllTerminals() {
// Add all sessions to visible set
for (const sessionId of this.sessions.keys()) {
this.visibleTerminals.add(sessionId);
}
-
+
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
/**
* Get the terminal grid container for the current tab
*/
@@ -5088,7 +5089,7 @@ class ClaudeOrchestrator {
const allSessions = Array.from(this.sessions.keys());
this.renderTerminalsWithVisibility(allSessions);
}
-
+
renderTerminalsWithVisibility(sessionIds) {
// Render all terminals but apply visibility using CSS (don't destroy DOM)
this.activeView = sessionIds.filter(id => this.isSessionVisibleInCurrentView(id));
@@ -5232,18 +5233,18 @@ class ClaudeOrchestrator {
this.resizeAllVisibleTerminals();
}, 200);
}
-
+
showClaudeOnly() {
this.setViewMode('claude');
}
-
+
showServersOnly() {
this.setViewMode('server');
}
-
+
applyPreset(preset) {
this.visibleTerminals.clear();
-
+
switch (preset) {
case 'all':
this.showAllTerminals();
@@ -5277,9 +5278,9 @@ class ClaudeOrchestrator {
break;
}
}
-
+
// changeLayout method removed - using dynamic layout based on visible terminal count
-
+
showTerminals(sessionIds) {
// Legacy function - update visible set and refresh everything
this.visibleTerminals.clear();
@@ -5291,31 +5292,31 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
renderTerminals(sessionIds) {
// Core rendering function - just displays terminals without updating state
this.activeView = sessionIds;
const grid = this.getTerminalGrid();
-
+
// Sort sessionIds to ensure proper ordering: work1-claude, work1-server, work2-claude, work2-server, etc.
const sortedSessionIds = sessionIds.slice().sort((a, b) => {
// Extract worktree number
const getWorkNum = (id) => parseInt(id.match(/work(\d+)/)?.[1] || 0);
const numA = getWorkNum(a);
const numB = getWorkNum(b);
-
+
// First sort by worktree number
if (numA !== numB) return numA - numB;
-
+
// Then claude before server
if (a.includes('claude') && b.includes('server')) return -1;
if (a.includes('server') && b.includes('claude')) return 1;
return 0;
});
-
+
// Clear grid but don't destroy terminals
grid.innerHTML = '';
-
+
// Create terminal elements for active view
sortedSessionIds.forEach((sessionId) => {
const session = this.sessions.get(sessionId);
@@ -5324,7 +5325,7 @@ class ClaudeOrchestrator {
grid.appendChild(wrapper);
}
});
-
+
// Now handle terminal instances
sortedSessionIds.forEach((sessionId, index) => {
const session = this.sessions.get(sessionId);
@@ -5332,25 +5333,25 @@ class ClaudeOrchestrator {
setTimeout(() => {
const terminalEl = document.getElementById(this.getSessionDomId('terminal', sessionId));
if (!terminalEl) return;
-
+
if (this.terminalManager.terminals.has(sessionId)) {
// Re-attach existing terminal to the new element
const term = this.terminalManager.terminals.get(sessionId);
-
+
// Clear and re-open the terminal in the new element
terminalEl.innerHTML = '';
term.open(terminalEl);
-
+
// Force a resize and refresh
this.terminalManager.fitTerminal(sessionId);
-
+
// Force a screen refresh to show content
term.refresh(0, term.rows - 1);
} else {
// Create new terminal only if it doesn't exist
this.terminalManager.createTerminal(sessionId, session);
}
-
+
// Don't auto-start Claude - let user choose via modal or button
}, 50 + (index * 25)); // Reduced stagger time
}
@@ -5576,41 +5577,41 @@ class ClaudeOrchestrator {
getTicketMetaForSession(sessionId, sessionOverride = null) {
const sid = String(sessionId || '').trim();
if (!sid) return null;
-
+
const session = sessionOverride || this.sessions.get(sid);
const recordIds = [];
recordIds.push(`session:${sid}`);
-
+
const worktreePath = session?.config?.cwd || session?.cwd || session?.worktreePath || null;
if (worktreePath) recordIds.push(`worktree:${worktreePath}`);
-
+
const prUrl = this.githubLinks.get(sid)?.pr || null;
const prTaskId = prUrl ? this.getPRTaskIdFromUrl(prUrl) : null;
if (prTaskId) recordIds.push(prTaskId);
-
+
for (const id of recordIds) {
const rec = this.taskRecords.get(id);
if (!rec) continue;
-
+
const ticketProvider = String(rec.ticketProvider || '').trim().toLowerCase();
const ticketCardId = String(rec.ticketCardId || '').trim();
const ticketCardUrl = String(rec.ticketCardUrl || '').trim();
const ticketTitle = String(rec.ticketTitle || '').trim();
-
+
if (!ticketTitle && !ticketCardId && !ticketCardUrl) continue;
-
+
const rawUrl =
ticketCardUrl
|| ((ticketProvider === 'trello' || !ticketProvider) && ticketCardId ? `https://trello.com/c/${ticketCardId}` : '');
const url = (rawUrl && /^https?:\/\//i.test(rawUrl)) ? rawUrl : '';
-
+
const label = ticketTitle || (ticketProvider && ticketCardId ? `${ticketProvider}:${ticketCardId}` : (ticketCardId ? ticketCardId : url));
const tooltipParts = [
ticketTitle || null,
ticketProvider && ticketCardId ? `${ticketProvider}:${ticketCardId}` : null,
url || null
].filter(Boolean);
-
+
return {
provider: ticketProvider || null,
cardId: ticketCardId || null,
@@ -5620,36 +5621,36 @@ class ClaudeOrchestrator {
tooltip: tooltipParts.join(' โข ')
};
}
-
+
return null;
}
-
+
updateTerminalTicketLabel(sessionId) {
const sid = String(sessionId || '').trim();
if (!sid) return;
-
+
const wrapper = this.getSessionWrapperElement(sid);
if (!wrapper) return;
-
+
const titleRow = wrapper.querySelector('.terminal-title');
if (!titleRow) return;
-
+
const existing = titleRow.querySelector('.terminal-ticket');
const meta = this.getTicketMetaForSession(sid);
-
+
if (!meta || !meta.label) {
existing?.remove();
return;
}
-
+
const label = `๐งพ ${meta.label}`;
const tooltip = meta.tooltip || meta.title || meta.label;
const url = meta.url || '';
-
+
const wantsLink = !!url;
const isLink = existing && existing.tagName && existing.tagName.toLowerCase() === 'a';
const isSpan = existing && existing.tagName && existing.tagName.toLowerCase() === 'span';
-
+
if (wantsLink) {
let el = existing;
if (!isLink) {
@@ -5658,7 +5659,7 @@ class ClaudeOrchestrator {
el.className = 'terminal-ticket';
el.target = '_blank';
el.rel = 'noopener noreferrer';
-
+
const branchEl = titleRow.querySelector('.terminal-branch');
if (branchEl && branchEl.parentElement === titleRow) {
branchEl.insertAdjacentElement('afterend', el);
@@ -5671,7 +5672,7 @@ class ClaudeOrchestrator {
el.title = tooltip;
return;
}
-
+
let el = existing;
if (!isSpan) {
existing?.remove();
@@ -5687,7 +5688,7 @@ class ClaudeOrchestrator {
el.textContent = label;
el.title = tooltip;
}
-
+
refreshBranchLabels() {
try {
for (const [sessionId, session] of this.sessions) {
@@ -5699,7 +5700,7 @@ class ClaudeOrchestrator {
}
this.buildSidebar();
}
-
+
createTerminalElement(sessionId, session) {
const wrapper = document.createElement('div');
wrapper.className = 'terminal-wrapper';
@@ -5709,7 +5710,7 @@ class ClaudeOrchestrator {
wrapper.addEventListener('mousedown', () => {
this.lastInteractedSessionId = sessionId;
});
-
+
const sessionType = String(session?.type || '').trim().toLowerCase();
const isAgentSession = sessionType === 'claude' || sessionType === 'codex';
const isServerSession = sessionType === 'server';
@@ -5819,7 +5820,7 @@ class ClaudeOrchestrator {
return wrapper;
}
-
+
updateSessionStatus(sessionId, status) {
const statusElement = document.getElementById(this.getSessionDomId('status', sessionId));
// Update session data
@@ -5903,7 +5904,7 @@ class ClaudeOrchestrator {
apply(status);
}
}
-
+
// Update quick actions for agent sessions
if (/-claude$|-codex$/.test(sessionId)) {
// Clear any pending notification timer if agent goes busy again
@@ -5935,11 +5936,11 @@ class ClaudeOrchestrator {
}, 3000);
}
}
-
+
// Update sidebar
this.updateSidebarStatus(sessionId, status);
}
-
+
updateSidebarStatus(sessionId, status) {
const session = this.sessions.get(sessionId);
const key = this.getSessionWorktreeKey(sessionId, session);
@@ -5996,7 +5997,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR) {
const session = this.sessions.get(sessionId);
if (session) {
@@ -6007,9 +6008,9 @@ class ClaudeOrchestrator {
if (defaultBranch) {
session.defaultBranch = defaultBranch;
}
-
+
console.log(`Branch updated for ${sessionId}: ${branch}`, existingPR ? `(existing PR: ${existingPR})` : '');
-
+
// If there's an existing PR, add it to GitHub links automatically
if (existingPR) {
const links = this.githubLinks.get(sessionId) || {};
@@ -6019,17 +6020,17 @@ class ClaudeOrchestrator {
this.maybeSchedulePrIntentRefresh(sessionId, existingPR);
}
}
-
+
// Update terminal branch display
this.updateTerminalBranchLabel(sessionId, branch || '');
-
+
// Update sidebar
this.buildSidebar();
-
+
// Update GitHub buttons with new remote URL
this.updateTerminalControls(sessionId);
}
-
+
// Server control methods
toggleServer(sessionId, environment = 'development') {
const status = this.serverStatuses.get(sessionId);
@@ -6056,22 +6057,22 @@ class ClaudeOrchestrator {
});
}
}
-
+
killServer(sessionId) {
// Send force kill
this.socket.emit('server-control', { sessionId, action: 'kill' });
this.serverStatuses.set(sessionId, 'idle');
-
+
// Update UI
const button = document.getElementById(`server-toggle-${sessionId}`);
if (button) {
button.textContent = 'โถ';
}
-
+
this.updateSidebarStatus(sessionId, 'idle');
this.updateServerControls(sessionId);
}
-
+
playInHytopia(sessionId) {
console.log(`[PLAY IN HYTOPIA] Session: ${sessionId}`);
console.log('Available ports:', Array.from(this.serverPorts.entries()));
@@ -6089,21 +6090,21 @@ class ClaudeOrchestrator {
}
return;
}
-
+
const serverUrl = `localhost:${port}`;
const hytopiaUrl = `https://hytopia.com/play/?${serverUrl}`;
-
+
console.log(`Opening Hytopia for ${sessionId} at ${hytopiaUrl}`);
window.open(hytopiaUrl, '_blank');
}
-
+
restoreBuildButton(sessionId) {
// Find any button that might be building for this worktree
const worktreeMatch = sessionId.match(/work(\d+)/);
if (!worktreeMatch) return;
-
+
const worktreeNum = worktreeMatch[1];
-
+
// Check both claude and server buttons for this worktree
[`work${worktreeNum}-claude`, `work${worktreeNum}-server`].forEach(id => {
const btn = this.buildingButtons?.get(id);
@@ -6115,7 +6116,7 @@ class ClaudeOrchestrator {
}
});
}
-
+
buildProduction(sessionId) {
// Extract worktree number from sessionId (e.g., 'work1-claude' -> 1)
const worktreeMatch = sessionId.match(/work(\d+)/);
@@ -6124,10 +6125,10 @@ class ClaudeOrchestrator {
this.showError('Failed to identify worktree for build');
return;
}
-
+
const worktreeNum = worktreeMatch[1];
console.log(`Building production ZIP for worktree ${worktreeNum}`);
-
+
// Disable the build button and show loading state
const wrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId));
const buildBtn = wrapper ? wrapper.querySelector('button[onclick*="buildProduction"]') : null;
@@ -6136,26 +6137,26 @@ class ClaudeOrchestrator {
buildBtn.innerHTML = '';
buildBtn.classList.add('building');
}
-
+
// Store the button reference for later
this.buildingButtons = this.buildingButtons || new Map();
this.buildingButtons.set(sessionId, buildBtn);
-
+
// Emit socket event to trigger build on backend
- this.socket.emit('build-production', {
+ this.socket.emit('build-production', {
sessionId,
- worktreeNum
+ worktreeNum
});
}
-
+
detectGitHubLinks(sessionId, data) {
// Look for GitHub URLs with improved pattern matching
const githubUrlPattern = /https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(?:\/(?!https:\/\/github\.com\/)[^\s\)\]\}\>\"\'\`]*)?/g;
const matches = data.match(githubUrlPattern);
-
+
if (matches) {
const links = this.githubLinks.get(sessionId) || {};
-
+
matches.forEach(originalUrl => {
// Clean up ANSI escape codes and other terminal artifacts
let url = originalUrl
@@ -6164,7 +6165,7 @@ class ClaudeOrchestrator {
.replace(/\u001b\[[0-9;]*m/g, '') // Remove Unicode ANSI codes
.replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove other control characters
.trim();
-
+
// Remove common trailing punctuation that might be captured
url = url.replace(/[,;.!?)\]}>'"`]*$/, '');
@@ -6173,7 +6174,7 @@ class ClaudeOrchestrator {
if (secondUrlIndex > 0) {
url = url.slice(0, secondUrlIndex);
}
-
+
// Validate URL format
try {
new URL(url);
@@ -6181,7 +6182,7 @@ class ClaudeOrchestrator {
console.warn('Invalid GitHub URL detected:', url);
return;
}
-
+
// Categorize the URL
if (url.includes('/pull/') && url.match(/\/pull\/\d+\/?$/)) {
if (links.pr !== url) {
@@ -6201,36 +6202,36 @@ class ClaudeOrchestrator {
}
}
});
-
+
this.githubLinks.set(sessionId, links);
this.updateTerminalControls(sessionId);
}
}
-
+
clearGitHubLinks(sessionId) {
this.githubLinks.delete(sessionId);
this.githubLinkLogs.delete(sessionId);
this.updateTerminalControls(sessionId);
}
-
+
copyLocalhostUrl(sessionId) {
const port = this.serverPorts.get(sessionId);
if (!port) {
console.error('No port found for server', sessionId);
return;
}
-
+
const url = `https://localhost:${port}`;
navigator.clipboard.writeText(url).then(() => {
console.log(`Copied ${url} to clipboard`);
this.showNotification('Copied!', `${url} copied to clipboard`);
});
}
-
+
openHytopiaWebsite() {
window.open('https://hytopia.com', '_blank');
}
-
+
openPRLink(url) {
try {
// Validate the URL
@@ -6242,17 +6243,17 @@ class ClaudeOrchestrator {
this.showToast('Invalid PR URL', 'error');
}
}
-
+
getGitHubButtons(sessionId) {
const links = this.githubLinks.get(sessionId) || {};
let buttons = '';
const visibility = this.getTerminalVisibilityConfig();
-
+
// Always show branch button (uses current session's git info)
const session = this.sessions.get(sessionId);
if (session && session.branch && session.branch !== 'master' && session.branch !== 'main') {
const worktreeId = sessionId.split('-')[0];
-
+
// Use dynamic remote URL if available
if (session.remoteUrl) {
const encodeRef = (ref) => encodeURIComponent(String(ref || '').trim());
@@ -6261,7 +6262,7 @@ class ClaudeOrchestrator {
// Use the actual default branch from git, fallback to 'main' if not available
const defaultBranch = session.defaultBranch || 'main';
const compareUrl = `${session.remoteUrl}/compare/${encodeRef(defaultBranch)}...${branchRef}`;
-
+
if (visibility.viewBranchOnGithub !== false) {
buttons += ``;
}
@@ -6273,7 +6274,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
// Show PR button if PR link detected
if (links.pr) {
const lastLogged = this.githubLinkLogs.get(sessionId);
@@ -6288,17 +6289,17 @@ class ClaudeOrchestrator {
buttons += ``;
}
}
-
+
// Check for commit URLs
if (links.commit) {
if (visibility.advancedDiff !== false) {
buttons += ``;
}
}
-
+
return buttons;
}
-
+
updateTerminalControls(sessionId) {
const wrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId));
if (!wrapper) return;
@@ -6378,43 +6379,43 @@ class ClaudeOrchestrator {
: '';
return [stopBtn, removeBtn].filter(Boolean).join('\n');
}
-
+
updateServerStatus(sessionId, output) {
// Check if server started - look for various startup messages
- if (output.includes('Server started') ||
- output.includes('Listening on') ||
+ if (output.includes('Server started') ||
+ output.includes('Listening on') ||
output.includes('Server running') ||
output.includes('Started server') ||
output.includes('๐')) {
this.serverStatuses.set(sessionId, 'running');
this.updateSidebarStatus(sessionId, 'running');
-
+
const button = document.getElementById(`server-toggle-${sessionId}`);
if (button) {
button.textContent = 'โน';
}
-
+
this.updateServerControls(sessionId);
const linkedClaude = this.getLinkedClaudeSessionIdForServer(sessionId);
if (linkedClaude) this.updateTerminalControls(linkedClaude);
}
-
+
// Check if server stopped
if (output.includes('Server stopped') || output.includes('exit')) {
this.serverStatuses.set(sessionId, 'idle');
this.updateSidebarStatus(sessionId, 'idle');
-
+
const button = document.getElementById(`server-toggle-${sessionId}`);
if (button) {
button.textContent = 'โถ';
}
-
+
this.updateServerControls(sessionId);
const linkedClaude = this.getLinkedClaudeSessionIdForServer(sessionId);
if (linkedClaude) this.updateTerminalControls(linkedClaude);
}
}
-
+
/**
* Get dynamic launch options based on current workspace
*/
@@ -6485,14 +6486,14 @@ class ClaudeOrchestrator {
// Use dynamic button system
controlsDiv.innerHTML = this.getServerControlsHTML(sessionId);
}
-
+
handleServerError(sessionId, output) {
const worktreeId = sessionId.split('-')[0];
-
+
// Update status
this.serverStatuses.set(sessionId, 'error');
this.updateSidebarStatus(sessionId, 'error');
-
+
// Show notification
this.notificationManager.handleNotification({
sessionId,
@@ -6518,13 +6519,13 @@ class ClaudeOrchestrator {
session.hasUserInput = false;
this.updateSidebarStatus(sid, String(session.status || 'idle').trim().toLowerCase() || 'idle');
}
-
+
sendTerminalInput(sessionId, data) {
if (!this.socket || !this.socket.connected) {
console.error('Not connected to server');
return;
}
-
+
// Mark session as active when user first provides input
// But only for meaningful input (not just arrow keys, etc.)
if (data.length > 0 && !data.match(/^[\x1b\x7f\r\n]/) && data.trim().length > 0) {
@@ -6547,7 +6548,7 @@ class ClaudeOrchestrator {
console.error('Failed to interrupt session', e);
}
}
-
+
resizeTerminal(sessionId, cols, rows) {
if (this.socket && this.socket.connected) {
this.socket.emit('terminal-resize', { sessionId, cols, rows });
@@ -7522,7 +7523,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
handleSessionRestart(sessionId) {
console.log(`Session ${sessionId} restarted`);
// Terminal will automatically reconnect and show new content
@@ -7551,20 +7552,20 @@ class ClaudeOrchestrator {
}
}
}
-
+
restartClaudeSession(sessionId) {
console.log(`Restarting Claude session: ${sessionId}`);
-
+
if (this.socket && this.socket.connected) {
this.socket.emit('restart-session', { sessionId });
-
+
// Update UI to show restarting
this.updateSessionStatus(sessionId, 'restarting');
} else {
this.showError('Not connected to server');
}
}
-
+
refreshTerminal(sessionId) {
console.log('Refreshing terminal:', sessionId);
const term = this.terminalManager.terminals.get(sessionId);
@@ -7572,10 +7573,10 @@ class ClaudeOrchestrator {
// Force fit and refresh
this.terminalManager.fitTerminal(sessionId);
term.refresh(0, term.rows - 1);
-
+
// Also try scrolling to bottom to trigger redraw
term.scrollToBottom();
-
+
// If still blank, re-attach to DOM
const terminalEl = document.getElementById(this.getSessionDomId('terminal', sessionId));
if (terminalEl && terminalEl.children.length === 0) {
@@ -7583,13 +7584,13 @@ class ClaudeOrchestrator {
}
}
}
-
+
updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
if (statusElement) {
const dot = statusElement.querySelector('.status-dot');
const text = statusElement.querySelector('span:last-child');
-
+
if (connected) {
dot.classList.remove('disconnected');
dot.classList.add('connected');
@@ -7601,12 +7602,12 @@ class ClaudeOrchestrator {
}
}
}
-
+
showError(message) {
// For now, use alert. Could be improved with a toast notification
alert(`Error: ${message}`);
}
-
+
showClaudeUpdateRequired(updateInfo) {
// Create update banner
const banner = document.createElement('div');
@@ -7621,10 +7622,10 @@ class ClaudeOrchestrator {
`;
-
+
// Add to top of page
document.body.insertBefore(banner, document.body.firstChild);
-
+
// Also show in console
console.warn('Claude Update Required:', updateInfo);
}
@@ -7661,36 +7662,18 @@ class ClaudeOrchestrator {
return;
}
this.lastNotificationTime[sessionId] = now;
-
+
const worktreeId = sessionId.replace('-claude', '');
const session = this.sessions.get(sessionId);
const branch = session ? session.branch : '';
-
- // Create small toast notification
- const toast = document.createElement('div');
- toast.className = 'ready-toast';
- toast.innerHTML = `
-
- โ
- Claude ${worktreeId} ready ${branch ? `(${branch})` : ''}
-
- `;
-
- // Add to page
- document.body.appendChild(toast);
-
- // Remove after 3 seconds
- setTimeout(() => {
- if (toast.parentNode) {
- toast.remove();
- }
- }, 3000);
-
+
+ this.showToast(`Claude ${worktreeId} ready ${branch ? `(${branch})` : ''}`, 'success', { durationMs: 3000 });
+
// Play notification sound if enabled
if (this.settings.sounds) {
this.playNotificationSound();
}
-
+
// Browser notification if enabled
if (this.settings.notifications && 'Notification' in window && Notification.permission === 'granted') {
new Notification(`Claude ${worktreeId} Ready`, {
@@ -7709,26 +7692,26 @@ class ClaudeOrchestrator {
});
}
}
-
+
playNotificationSound() {
// Create a simple notification sound
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
-
+
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
-
+
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
-
+
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
-
+
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}
-
+
loadSettings() {
const stored = localStorage.getItem('claude-orchestrator-settings');
const defaults = {
@@ -8501,11 +8484,11 @@ class ClaudeOrchestrator {
globalNodeInput.value = nodeOptions.join(' ');
globalArgsInput.value = gameArgs.join(' ');
}
-
+
saveSettings() {
localStorage.setItem('claude-orchestrator-settings', JSON.stringify(this.settings));
}
-
+
applyTheme() {
const mode = this.settings.theme === 'light' ? 'light' : 'dark';
document.body.classList.toggle('light-theme', mode === 'light');
@@ -8683,7 +8666,7 @@ class ClaudeOrchestrator {
}
return normalized;
}
-
+
syncSettingsUI() {
// Sync checkbox states with settings
document.getElementById('enable-notifications').checked = this.settings.notifications;
@@ -8702,29 +8685,29 @@ class ClaudeOrchestrator {
const label = document.getElementById('skin-intensity-value');
if (label) label.textContent = `${v}%`;
}
-
+
// Sync user settings UI if loaded
if (this.userSettings) {
this.syncUserSettingsUI();
}
}
-
+
showCodeReviewDropdown(sessionId) {
// Close any existing dropdowns
document.querySelectorAll('.review-dropdown').forEach(dropdown => dropdown.remove());
-
+
// Get the terminal controls container
const terminalWrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId));
const controlsContainer = terminalWrapper.querySelector('.terminal-controls');
-
+
// Create dropdown
const dropdown = document.createElement('div');
dropdown.className = 'review-dropdown';
dropdown.innerHTML = this.buildReviewerDropdownHTML(sessionId);
-
+
// Position and add to DOM
controlsContainer.appendChild(dropdown);
-
+
// Close dropdown when clicking outside
const closeDropdown = (e) => {
if (!dropdown.contains(e.target)) {
@@ -8732,22 +8715,22 @@ class ClaudeOrchestrator {
document.removeEventListener('click', closeDropdown);
}
};
-
+
// Add close listener after a short delay to prevent immediate closure
setTimeout(() => {
document.addEventListener('click', closeDropdown);
}, 100);
}
-
+
buildReviewerDropdownHTML(requestingSessionId) {
const availableReviewers = this.getAvailableReviewers(requestingSessionId);
-
+
let html = `
`;
-
+
if (availableReviewers.length === 0) {
html += `
@@ -8767,19 +8750,19 @@ class ClaudeOrchestrator {
`;
});
}
-
+
return html;
}
-
+
getAvailableReviewers(requestingSessionId) {
const reviewers = [];
-
+
for (const [sessionId, session] of this.sessions) {
// Only include Claude sessions that are not the requesting session
if (sessionId.includes('-claude') && sessionId !== requestingSessionId) {
const worktreeNumber = sessionId.replace('-claude', '').replace('work', '');
const isActive = this.sessionActivity.get(sessionId) === 'active';
-
+
// Prefer active sessions, but include inactive ones as backup
if (isActive || session.status === 'waiting') {
reviewers.push({
@@ -8792,7 +8775,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
// Sort by preference: active + ready first, then active + busy, then inactive
reviewers.sort((a, b) => {
if (a.isActive && !b.isActive) return -1;
@@ -8801,48 +8784,48 @@ class ClaudeOrchestrator {
if (a.status !== 'waiting' && b.status === 'waiting') return 1;
return 0;
});
-
+
return reviewers;
}
-
+
async assignCodeReview(requestingSessionId, reviewerSessionId) {
// Close dropdown
document.querySelectorAll('.review-dropdown').forEach(dropdown => dropdown.remove());
-
+
try {
// Extract code/PR information from the requesting session
const codeInfo = await this.extractCodeForReview(requestingSessionId);
-
+
if (!codeInfo.hasContent) {
this.showToast(`No code changes detected in Claude ${requestingSessionId.replace('work', '').replace('-claude', '')}`, 'warning');
return;
}
-
+
// Format review request
const reviewRequest = this.formatReviewRequest(codeInfo, requestingSessionId);
-
+
// Send to reviewer Claude
this.sendTerminalInput(reviewerSessionId, reviewRequest);
-
+
// Mark both sessions as active
this.sessionActivity.set(reviewerSessionId, 'active');
this.buildSidebar();
-
+
// Show success message
const requestingWorktree = requestingSessionId.replace('work', '').replace('-claude', '');
const reviewerWorktree = reviewerSessionId.replace('work', '').replace('-claude', '');
this.showToast(`Code review assigned: Claude ${requestingWorktree} โ Claude ${reviewerWorktree}`, 'success');
-
+
} catch (error) {
console.error('Error assigning code review:', error);
this.showToast('Failed to assign code review', 'error');
}
}
-
+
async extractCodeForReview(sessionId) {
// Get terminal content
const terminalContent = this.terminalManager.getTerminalContent(sessionId);
-
+
// Look for various types of code content
const codePatterns = {
prUrl: /https:\/\/github\.com\/[^\s]+\/pull\/\d+/g,
@@ -8851,7 +8834,7 @@ class ClaudeOrchestrator {
codeBlocks: /```[\s\S]*?```/g,
bashCommands: /(?:git\s+(?:diff|log|show)|gh\s+pr)/g
};
-
+
const extracted = {
prUrls: [...(terminalContent.match(codePatterns.prUrl) || [])],
gitDiffs: [...(terminalContent.match(codePatterns.gitDiff) || [])],
@@ -8859,20 +8842,20 @@ class ClaudeOrchestrator {
recentCommands: this.extractRecentCommands(terminalContent),
hasContent: false
};
-
+
// Determine if there's reviewable content
- extracted.hasContent = extracted.prUrls.length > 0 ||
- extracted.gitDiffs.length > 0 ||
+ extracted.hasContent = extracted.prUrls.length > 0 ||
+ extracted.gitDiffs.length > 0 ||
extracted.codeBlocks.length > 0 ||
extracted.recentCommands.some(cmd => cmd.includes('git') || cmd.includes('gh pr'));
-
+
return extracted;
}
-
+
extractRecentCommands(terminalContent) {
const lines = terminalContent.split('\n');
const commands = [];
-
+
// Look for command patterns (simple approach)
for (let i = lines.length - 1; i >= 0 && commands.length < 10; i--) {
const line = lines[i].trim();
@@ -8880,15 +8863,15 @@ class ClaudeOrchestrator {
commands.unshift(line);
}
}
-
+
return commands;
}
-
+
formatReviewRequest(codeInfo, requestingSessionId) {
const requestingWorktree = requestingSessionId.replace('work', '').replace('-claude', '');
-
+
let request = `Please review the code from Claude ${requestingWorktree}:\n\n`;
-
+
if (codeInfo.prUrls.length > 0) {
request += `**Pull Request(s):**\n`;
codeInfo.prUrls.forEach(url => {
@@ -8900,19 +8883,19 @@ class ClaudeOrchestrator {
request += `- Suggestions for improvement\n`;
request += `- Architecture and design patterns\n\n`;
}
-
+
if (codeInfo.gitDiffs.length > 0) {
request += `**Git Diff:**\n\`\`\`diff\n`;
request += codeInfo.gitDiffs.slice(0, 2).join('\n'); // Limit to first 2 diffs
request += `\n\`\`\`\n\n`;
}
-
+
if (codeInfo.codeBlocks.length > 0) {
request += `**Code Changes:**\n`;
request += codeInfo.codeBlocks.slice(0, 3).join('\n\n'); // Limit to first 3 blocks
request += `\n\n`;
}
-
+
if (codeInfo.recentCommands.length > 0) {
request += `**Recent Commands:**\n`;
codeInfo.recentCommands.forEach(cmd => {
@@ -8920,9 +8903,9 @@ class ClaudeOrchestrator {
});
request += `\n`;
}
-
+
request += `Please provide a thorough code review with specific feedback and suggestions.\n`;
-
+
return request;
}
@@ -9250,75 +9233,1500 @@ class ClaudeOrchestrator {
throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`));
}
- const diagnostics = data?.diagnostics;
- if (diagnostics && typeof diagnostics === 'object') {
- state.firstRun = diagnostics;
- renderRepairActions(state.firstRun);
- } else {
- await refreshFirstRun();
+ const diagnostics = data?.diagnostics;
+ if (diagnostics && typeof diagnostics === 'object') {
+ state.firstRun = diagnostics;
+ renderRepairActions(state.firstRun);
+ } else {
+ await refreshFirstRun();
+ }
+ if (!state.base) await refreshBase();
+ await refreshInstallWizard().catch(() => {});
+ render(state.base, state.firstRun, state.wizard);
+
+ const appliedCount = Number(data?.appliedCount || 0);
+ const failedCount = Number(data?.failedCount || 0);
+ const skippedManualCount = Number(data?.skippedManualCount || 0);
+ if (failedCount > 0) {
+ this.showToast?.(`Auto-fix applied ${appliedCount}, failed ${failedCount}`, 'warning');
+ } else {
+ const tail = skippedManualCount > 0 ? `, ${skippedManualCount} manual step(s) left` : '';
+ this.showToast?.(`Auto-fix applied ${appliedCount}${tail}`, 'success');
+ }
+ if (statusEl) statusEl.textContent = 'Safe auto-fix completed';
+ } catch (error) {
+ this.showToast?.(`Safe auto-fix failed: ${String(error?.message || error)}`, 'error');
+ if (statusEl) statusEl.textContent = '';
+ } finally {
+ btnRepairSafe.disabled = false;
+ }
+ });
+ repairEl?.addEventListener('click', async (event) => {
+ const target = event.target.closest('[data-diagnostics-repair]');
+ if (!target) return;
+ const action = String(target.getAttribute('data-diagnostics-repair') || '').trim();
+ if (!action) return;
+ target.disabled = true;
+ if (statusEl) statusEl.textContent = `Running repair: ${action}โฆ`;
+ try {
+ const res = await fetch('/api/diagnostics/first-run/repair', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action })
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false) {
+ throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`));
+ }
+ const repair = data?.repair || {};
+ if (repair.manual) {
+ this.showToast?.(String(repair?.message || 'Manual action required'), 'warning');
+ } else {
+ this.showToast?.(String(repair?.message || 'Repair completed'), 'success');
+ }
+ if (data?.diagnostics) {
+ state.firstRun = data.diagnostics;
+ renderRepairActions(state.firstRun);
+ } else {
+ await refreshFirstRun();
+ }
+ if (!state.base) await refreshBase();
+ await refreshInstallWizard().catch(() => {});
+ render(state.base, state.firstRun, state.wizard);
+ if (statusEl) statusEl.textContent = `Repair completed: ${action}`;
+ } catch (error) {
+ this.showToast?.(`Repair failed: ${String(error?.message || error)}`, 'error');
+ if (statusEl) statusEl.textContent = '';
+ } finally {
+ target.disabled = false;
+ }
+ });
+ }
+
+ openDiagnosticsPanel({ refresh = true } = {}) {
+ try {
+ document.getElementById('settings-panel')?.classList?.remove?.('hidden');
+ setTimeout(() => {
+ try {
+ document.getElementById('diagnostics-output')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
+ } catch {
+ // ignore
+ }
+ if (!refresh) return;
+ if (typeof this.refreshDiagnosticsPanel === 'function') {
+ this.refreshDiagnosticsPanel();
+ return;
+ }
+ try {
+ document.getElementById('diagnostics-refresh')?.click?.();
+ } catch {
+ // ignore
+ }
+ }, 50);
+ } catch {
+ // ignore
+ }
+ }
+
+ setupDependencySetupWizard() {
+ const modal = document.getElementById('dependency-setup-modal');
+ const openBtn = document.getElementById('dependency-setup-open');
+ const summaryEl = document.getElementById('dependency-setup-summary');
+ const listEl = document.getElementById('dependency-setup-list');
+ const closeBtn = document.getElementById('dependency-setup-close');
+ if (!modal || !summaryEl || !listEl) return;
+ const body = document.body;
+ const isWindowsHost = (() => {
+ try {
+ const platform = String(navigator?.platform || '').toLowerCase();
+ const userAgent = String(navigator?.userAgent || '').toLowerCase();
+ return platform.includes('win') || userAgent.includes('windows');
+ } catch {
+ return false;
+ }
+ })();
+
+ const setBootstrapPending = (pending) => {
+ if (pending) {
+ if (!isWindowsHost) return;
+ body?.classList?.add?.('dependency-onboarding-booting');
+ body?.classList?.remove?.('dependency-onboarding-active');
+ return;
+ }
+ body?.classList?.remove?.('dependency-onboarding-booting');
+ };
+ if (!isWindowsHost) {
+ setBootstrapPending(false);
+ return;
+ }
+
+ const dismissKey = 'orchestrator-dependency-setup-dismissed-v3';
+ const completedKey = 'orchestrator-dependency-onboarding-completed-v2';
+ const progressKey = 'orchestrator-dependency-onboarding-progress-v2';
+ const skippedStepsKey = 'orchestrator-dependency-onboarding-skipped-v1';
+ const setupStateUrl = '/api/setup-actions/state';
+ const readLegacyBool = (key) => {
+ try {
+ return localStorage.getItem(key) === 'true';
+ } catch {
+ return false;
+ }
+ };
+ const readLegacyStep = () => {
+ try {
+ const raw = Number.parseInt(String(localStorage.getItem(progressKey) || ''), 10);
+ if (Number.isFinite(raw) && raw >= 0) return raw;
+ return 0;
+ } catch {
+ return 0;
+ }
+ };
+ const readLegacySkippedActionIds = () => {
+ try {
+ const raw = localStorage.getItem(skippedStepsKey);
+ if (!raw) return [];
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return [];
+ const seen = new Set();
+ return parsed
+ .map((value) => String(value || '').trim())
+ .filter((value) => {
+ if (!value || seen.has(value)) return false;
+ seen.add(value);
+ return true;
+ });
+ } catch {
+ return [];
+ }
+ };
+ const normalizeSetupState = (value) => {
+ const currentStepRaw = Number.parseInt(String(value?.currentStep ?? 0), 10);
+ const skippedActionIds = Array.isArray(value?.skippedActionIds) ? value.skippedActionIds : [];
+ const seen = new Set();
+ return {
+ dismissed: value?.dismissed === true,
+ completed: value?.completed === true,
+ currentStep: Number.isFinite(currentStepRaw) && currentStepRaw >= 0 ? currentStepRaw : 0,
+ skippedActionIds: skippedActionIds
+ .map((entry) => String(entry || '').trim())
+ .filter((entry) => {
+ if (!entry || seen.has(entry)) return false;
+ seen.add(entry);
+ return true;
+ })
+ };
+ };
+ const readBootstrapSetupState = () => {
+ try {
+ const bootstrapState = window.__ORCHESTRATOR_SETUP_STATE__;
+ if (bootstrapState && typeof bootstrapState === 'object') {
+ return normalizeSetupState(bootstrapState);
+ }
+ } catch {
+ // ignore and fall back to legacy/local state
+ }
+ return normalizeSetupState({
+ dismissed: readLegacyBool(dismissKey),
+ completed: readLegacyBool(completedKey),
+ currentStep: readLegacyStep(),
+ skippedActionIds: readLegacySkippedActionIds()
+ });
+ };
+ const persistedSetupState = {
+ loaded: false,
+ loadPromise: null,
+ savePromise: Promise.resolve(),
+ current: readBootstrapSetupState()
+ };
+ const syncLegacySetupState = () => {
+ const current = persistedSetupState.current || normalizeSetupState({});
+ try {
+ if (current.dismissed) localStorage.setItem(dismissKey, 'true');
+ else localStorage.removeItem(dismissKey);
+ if (current.completed) localStorage.setItem(completedKey, 'true');
+ else localStorage.removeItem(completedKey);
+ localStorage.setItem(progressKey, String(Math.max(0, Number(current.currentStep) || 0)));
+ if (Array.isArray(current.skippedActionIds) && current.skippedActionIds.length > 0) {
+ localStorage.setItem(skippedStepsKey, JSON.stringify(current.skippedActionIds));
+ } else {
+ localStorage.removeItem(skippedStepsKey);
+ }
+ } catch {
+ // ignore
+ }
+ };
+ const getPersistedSetupState = () => ({
+ ...(persistedSetupState.current || normalizeSetupState({})),
+ skippedActionIds: Array.isArray(persistedSetupState.current?.skippedActionIds)
+ ? [...persistedSetupState.current.skippedActionIds]
+ : []
+ });
+ const applyPersistedSetupState = (value) => {
+ persistedSetupState.current = normalizeSetupState(value || {});
+ syncLegacySetupState();
+ return getPersistedSetupState();
+ };
+ const loadPersistedSetupState = async ({ force = false } = {}) => {
+ if (persistedSetupState.loaded && !force) return getPersistedSetupState();
+ if (persistedSetupState.loadPromise && !force) return persistedSetupState.loadPromise;
+ persistedSetupState.loadPromise = (async () => {
+ try {
+ const res = await fetch(setupStateUrl);
+ const data = await res.json().catch(() => ({}));
+ if (res.ok && data?.ok !== false) {
+ applyPersistedSetupState(data?.state || {});
+ }
+ } catch {
+ // ignore and keep local fallback state
+ } finally {
+ persistedSetupState.loaded = true;
+ persistedSetupState.loadPromise = null;
+ }
+ return getPersistedSetupState();
+ })();
+ return persistedSetupState.loadPromise;
+ };
+ const savePersistedSetupState = async (patch = {}) => {
+ applyPersistedSetupState({
+ ...(persistedSetupState.current || normalizeSetupState({})),
+ ...((patch && typeof patch === 'object') ? patch : {})
+ });
+ persistedSetupState.savePromise = persistedSetupState.savePromise
+ .catch(() => null)
+ .then(async () => {
+ try {
+ const res = await fetch(setupStateUrl, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(patch || {})
+ });
+ const data = await res.json().catch(() => ({}));
+ if (res.ok && data?.ok !== false) {
+ applyPersistedSetupState(data?.state || {});
+ }
+ } catch {
+ // ignore and keep local state
+ }
+ return getPersistedSetupState();
+ });
+ return persistedSetupState.savePromise;
+ };
+ setBootstrapPending(true);
+
+ const state = {
+ loading: false,
+ diagnostics: null,
+ actions: [],
+ currentStep: 0,
+ showWelcome: true,
+ skippedActionIds: new Set(),
+ actionRuns: new Map(),
+ actionRunPollers: new Map(),
+ gitIdentity: {
+ name: '',
+ email: ''
+ },
+ gitIdentityHelpVisible: false,
+ startupPending: true
+ };
+
+ const readDismissed = () => getPersistedSetupState().dismissed === true;
+
+ const writeDismissed = (value) => {
+ const next = value === true;
+ if (readDismissed() === next) return;
+ void savePersistedSetupState({ dismissed: next });
+ };
+
+ const readCompleted = () => getPersistedSetupState().completed === true;
+
+ const writeCompleted = (value) => {
+ const next = value === true;
+ if (readCompleted() === next) return;
+ void savePersistedSetupState({ completed: next });
+ };
+
+ const readSavedStep = () => Math.max(0, Number(getPersistedSetupState().currentStep) || 0);
+
+ const writeSavedStep = (step) => {
+ const next = Math.max(0, Number(step) || 0);
+ if (readSavedStep() === next) return;
+ void savePersistedSetupState({ currentStep: next });
+ };
+
+ const readSkippedStepIds = () => new Set(getPersistedSetupState().skippedActionIds);
+
+ const writeSkippedStepIds = () => {
+ const next = Array.from(state.skippedActionIds || [])
+ .map((value) => String(value || '').trim())
+ .filter(Boolean);
+ const prev = getPersistedSetupState().skippedActionIds || [];
+ if (next.length === prev.length && next.every((value, index) => value === prev[index])) return;
+ void savePersistedSetupState({ skippedActionIds: next });
+ };
+
+ const setStepSkipped = (actionId, skipped) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ if (skipped) state.skippedActionIds.add(id);
+ else state.skippedActionIds.delete(id);
+ writeSkippedStepIds();
+ };
+
+ const toToolMap = (diagnostics) => {
+ const map = new Map();
+ const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : [];
+ tools.forEach((tool) => {
+ const id = String(tool?.id || '').trim();
+ if (!id) return;
+ map.set(id, !!tool?.ok);
+ });
+ return map;
+ };
+
+ const getToolResult = (diagnostics, toolId) => {
+ const id = String(toolId || '').trim();
+ if (!id) return null;
+ const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : [];
+ return tools.find((tool) => String(tool?.id || '').trim() === id) || null;
+ };
+
+ const parseGitIdentityVersion = (value) => {
+ const raw = String(value || '').trim();
+ if (!raw) return { name: '', email: '' };
+ const pair = raw.match(/^(.*)\s<([^<>]+)>$/);
+ if (pair?.[1] && pair?.[2]) {
+ return {
+ name: String(pair[1] || '').trim(),
+ email: String(pair[2] || '').trim()
+ };
+ }
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) {
+ return { name: '', email: raw };
+ }
+ return { name: raw, email: '' };
+ };
+
+ const stripAnsiText = (value) => String(value || '').replace(/\u001b\[[0-9;]*m/g, '');
+
+ const collectRunOutputLines = (runInfo, { limit = 25 } = {}) => {
+ const lines = Array.isArray(runInfo?.output)
+ ? runInfo.output
+ .map((entry) => stripAnsiText(String(entry?.line || '')).trim())
+ .filter(Boolean)
+ : [];
+ if (!Number.isFinite(limit) || limit <= 0) return lines;
+ return lines.slice(-Math.max(1, Number(limit) || 1));
+ };
+
+ const extractGithubLoginInfo = (lines = []) => {
+ const fallbackUrl = 'https://github.com/login/device';
+ let link = fallbackUrl;
+ let code = '';
+ let sawDeviceHint = false;
+
+ (Array.isArray(lines) ? lines : []).forEach((lineRaw) => {
+ const line = String(lineRaw || '').trim();
+ if (!line) return;
+
+ if (/one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i.test(line)) {
+ sawDeviceHint = true;
+ }
+
+ const linkMatch = line.match(/https:\/\/github\.com\/login\/device(?:\S*)?/i);
+ if (linkMatch?.[0]) link = linkMatch[0].trim();
+
+ const codeMatch = line.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i);
+ if (codeMatch?.[1]) code = String(codeMatch[1]).toUpperCase();
+ });
+
+ return {
+ link,
+ code,
+ sawDeviceHint
+ };
+ };
+
+ const hydrateGitIdentityDraft = (diagnostics) => {
+ const gitIdentityTool = getToolResult(diagnostics, 'gitIdentity');
+ const parsed = parseGitIdentityVersion(String(gitIdentityTool?.version || ''));
+ if (!state.gitIdentity.name && parsed.name) {
+ state.gitIdentity.name = parsed.name;
+ }
+ if (!state.gitIdentity.email && parsed.email) {
+ state.gitIdentity.email = parsed.email;
+ }
+ };
+
+ const getRequirementState = (toolsMap) => {
+ const gitOk = !!toolsMap.get('git');
+ const claudeOk = !!toolsMap.get('claude');
+ const codexOk = !!toolsMap.get('codex');
+ const hasAgentCli = claudeOk || codexOk;
+ const coreReady = gitOk && hasAgentCli;
+ const missingCore = [];
+ if (!gitOk) missingCore.push('git');
+ if (!hasAgentCli) missingCore.push('agent-cli');
+ return {
+ gitOk,
+ claudeOk,
+ codexOk,
+ hasAgentCli,
+ coreReady,
+ missingCore
+ };
+ };
+
+ const isActionComplete = (actionId, toolsMap) => {
+ switch (String(actionId || '').trim()) {
+ case 'install-git':
+ return !!toolsMap.get('git');
+ case 'configure-git-identity':
+ return !!toolsMap.get('gitIdentity');
+ case 'install-node':
+ return !!toolsMap.get('node') && !!toolsMap.get('npm');
+ case 'install-gh':
+ return !!toolsMap.get('gh');
+ case 'gh-login':
+ return !!toolsMap.get('ghAuth');
+ case 'install-claude':
+ return !!toolsMap.get('claude');
+ case 'install-codex':
+ return !!toolsMap.get('codex');
+ default:
+ return false;
+ }
+ };
+
+ const getActionLevelText = (level) => {
+ if (level === 'required') return 'Required';
+ if (level === 'optional') return 'Optional';
+ if (level === 'core-option') return 'Core option';
+ return 'Recommended';
+ };
+
+ const getActionLevelClass = (level) => {
+ if (level === 'optional') return 'level-optional';
+ return level === 'recommended' ? 'level-recommended' : 'level-required';
+ };
+
+ const getActionStatusText = (actionId, done) => {
+ const id = String(actionId || '').trim();
+ if (id === 'gh-login') return done ? 'Logged in' : 'Not logged in';
+ if (id === 'configure-git-identity') return done ? 'Configured' : 'Not configured';
+ return done ? 'Installed' : 'Missing';
+ };
+
+ const getResolvedSteps = () => {
+ const toolsMap = toToolMap(state.diagnostics);
+ const actions = Array.isArray(state.actions) ? state.actions : [];
+ return actions.map((action) => {
+ const id = String(action?.id || '').trim();
+ const level = getActionLevel(id);
+ const done = isActionComplete(id, toolsMap);
+ return {
+ ...action,
+ id,
+ level,
+ optional: action?.optional === true || level === 'optional',
+ done,
+ levelText: getActionLevelText(level),
+ levelClass: getActionLevelClass(level),
+ statusText: getActionStatusText(id, done),
+ statusClass: done ? 'status-ok' : 'status-missing',
+ runSupported: action?.runSupported !== false
+ };
+ });
+ };
+
+ const syncSkippedSteps = (steps) => {
+ if (!(state.skippedActionIds instanceof Set)) {
+ state.skippedActionIds = new Set();
+ }
+ const validSkippedIds = new Set(
+ (Array.isArray(steps) ? steps : [])
+ .filter((step) => {
+ const id = String(step?.id || '').trim();
+ return !!id && step?.optional && !step?.done;
+ })
+ .map((step) => String(step?.id || '').trim())
+ );
+ let changed = false;
+ for (const id of Array.from(state.skippedActionIds)) {
+ if (!validSkippedIds.has(id)) {
+ state.skippedActionIds.delete(id);
+ changed = true;
+ }
+ }
+ if (changed) writeSkippedStepIds();
+ };
+
+ const isOnboardingLocked = () => {
+ if (!isWindowsHost) return false;
+ if (readCompleted()) return false;
+ if (!Array.isArray(state.actions) || state.actions.length === 0) return false;
+ const toolsMap = toToolMap(state.diagnostics);
+ const req = getRequirementState(toolsMap);
+ return !req?.coreReady;
+ };
+
+ const applyOnboardingLockUI = () => {
+ const locked = isOnboardingLocked();
+ if (closeBtn) {
+ closeBtn.disabled = locked;
+ closeBtn.style.visibility = locked ? 'hidden' : '';
+ }
+ modal.setAttribute('data-onboarding-locked', locked ? 'true' : 'false');
+ return locked;
+ };
+
+ const setCurrentStep = (nextStep, { persist = true } = {}) => {
+ const previousStep = state.currentStep;
+ const maxStep = Math.max(0, (Array.isArray(state.actions) ? state.actions.length : 0) - 1);
+ const parsed = Number.parseInt(String(nextStep), 10);
+ const safe = Number.isFinite(parsed) ? parsed : 0;
+ state.currentStep = Math.max(0, Math.min(safe, maxStep));
+ if (state.currentStep !== previousStep) {
+ state.gitIdentityHelpVisible = false;
+ }
+ if (persist) writeSavedStep(state.currentStep);
+ return state.currentStep;
+ };
+
+ const getActionLevel = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (id === 'install-git') return 'required';
+ if (id === 'configure-git-identity') return 'optional';
+ if (id === 'install-gh' || id === 'gh-login') return 'optional';
+ if (id === 'install-claude') return 'optional';
+ if (id === 'install-codex') return 'optional';
+ return 'recommended';
+ };
+
+ const buildStepIconSvg = (iconMarkup) => (
+ `
`
+ );
+
+ const stepIconSvgByActionId = Object.freeze({
+ 'install-git': buildStepIconSvg('
'),
+ 'configure-git-identity': buildStepIconSvg('
'),
+ 'install-node': buildStepIconSvg('
'),
+ 'install-gh': buildStepIconSvg('
'),
+ 'gh-login': buildStepIconSvg('
'),
+ 'install-claude': buildStepIconSvg('
'),
+ 'install-codex': buildStepIconSvg('
')
+ });
+
+ const getStepIconSvg = (actionId) => {
+ const id = String(actionId || '').trim();
+ return stepIconSvgByActionId[id]
+ || buildStepIconSvg('
');
+ };
+
+ const render = () => {
+ const toolsMap = toToolMap(state.diagnostics);
+ const req = getRequirementState(toolsMap);
+ const steps = getResolvedSteps();
+ syncSkippedSteps(steps);
+ if (!steps.length) {
+ summaryEl.textContent = 'No setup actions are available for this platform.';
+ listEl.innerHTML = '
No setup actions are available for this platform.
';
+ return { req, steps, current: null };
+ }
+
+ setCurrentStep(state.currentStep, { persist: false });
+ const current = steps[state.currentStep];
+ const stepNo = state.currentStep + 1;
+ const totalSteps = steps.length;
+ const detectedCount = steps.filter((step) => step.done).length;
+ const doneRatio = totalSteps > 0 ? Math.round((detectedCount / totalSteps) * 100) : 0;
+ const missingCore = [];
+ if (!req.gitOk) missingCore.push('Git');
+ if (!req.hasAgentCli) missingCore.push('Claude Code or Codex CLI');
+
+ if (state.showWelcome) {
+ summaryEl.textContent = '';
+ listEl.innerHTML = `
+
+
Letโs get you ready in a minute.
+
+ Weโll check your system and install whatโs needed.
+ Optional tools can be skipped.
+
+
+
+
+
`;
+ return { req, steps, current };
+ }
+
+ summaryEl.textContent = '';
+
+ const currentId = String(current?.id || '').trim();
+ const currentStepIconSvg = getStepIconSvg(currentId);
+ const currentTitle = this.escapeHtml(String(current?.title || currentId || 'Setup action'));
+ const currentDesc = this.escapeHtml(String(current?.description || ''));
+ const commandRaw = String(current?.command || '');
+ const runInfo = state.actionRuns.get(currentId) || null;
+ const runStatus = String(runInfo?.status || '').trim().toLowerCase();
+ const isRunning = runStatus === 'running';
+ const isVerifying = runStatus === 'verifying';
+ const isFinalizing = runStatus === 'success' || runStatus === 'completed';
+ const isRunBusy = isRunning || isVerifying || isFinalizing;
+ const isGitIdentityStep = currentId === 'configure-git-identity';
+ const runOutputAll = collectRunOutputLines(runInfo, { limit: 160 });
+ const runOutput = runOutputAll.slice(-8);
+ const runOutputText = this.escapeHtml(runOutput.join('\n'));
+ const shouldShowInstallerOutput = currentId !== 'gh-login' && !isGitIdentityStep && (
+ runOutput.length > 0 ||
+ isRunBusy ||
+ runStatus === 'failed' ||
+ runStatus === 'needs-attention'
+ );
+ const installerOutputText = runOutput.length
+ ? runOutputText
+ : this.escapeHtml(
+ isRunning
+ ? 'Installer started. Waiting for output...'
+ : (isVerifying
+ ? 'Installer finished. Verifying dependency...'
+ : 'No installer output captured yet.')
+ );
+ const githubDeviceUrl = 'https://github.com/login/device';
+ const ghInstalled = !!toolsMap.get('gh');
+ const ghLoggedIn = !!toolsMap.get('ghAuth');
+ const ghLoginRunInfo = state.actionRuns.get('gh-login') || null;
+ const ghLoginRunStatus = String(ghLoginRunInfo?.status || '').trim().toLowerCase();
+ const ghLoginIsRunning = ghLoginRunStatus === 'running';
+ const ghLoginIsVerifying = ghLoginRunStatus === 'verifying';
+ const ghLoginIsFinalizing = ghLoginRunStatus === 'success' || ghLoginRunStatus === 'completed';
+ const ghLoginIsBusy = ghLoginIsRunning || ghLoginIsVerifying || ghLoginIsFinalizing;
+ const ghLoginOutputAll = collectRunOutputLines(ghLoginRunInfo, { limit: 160 });
+ const ghLoginInfo = extractGithubLoginInfo(ghLoginOutputAll);
+ const ghLoginLink = String(ghLoginRunInfo?.ghDeviceUrl || ghLoginInfo.link || githubDeviceUrl).trim() || githubDeviceUrl;
+ const ghLoginCode = String(ghLoginRunInfo?.ghDeviceCode || ghLoginInfo.code || '').trim().toUpperCase();
+ const ghLoginHasSignal = !!(
+ ghLoginRunInfo?.ghHasDeviceHint
+ || ghLoginInfo.sawDeviceHint
+ || ghLoginCode
+ || ghLoginLink !== githubDeviceUrl
+ );
+ const ghLoginUiPhase = (() => {
+ if (!ghInstalled || ghLoggedIn) return 'none';
+ if (!ghLoginRunInfo) return 'start';
+ if (ghLoginCode) return 'code';
+ if (ghLoginIsBusy) return 'wait-code';
+ return 'retry';
+ })();
+ const ghLoginInlineStatusText = (() => {
+ if (!ghInstalled) return 'Install GitHub CLI first';
+ if (ghLoggedIn) return 'Logged in';
+ if (ghLoginIsFinalizing) return 'Finalizing login';
+ if (ghLoginIsRunning) return 'Signing in';
+ if (ghLoginIsVerifying) return 'Checking login';
+ return 'Not logged in';
+ })();
+ const ghLoginInlineStatusClass = ghLoggedIn
+ ? 'status-ok'
+ : ((ghLoginIsBusy || ghLoginRunStatus === 'needs-attention') ? 'status-pending' : 'status-missing');
+ const ghLoginInlineRunLabel = (() => {
+ if (ghLoggedIn) return 'Logged in';
+ if (ghLoginIsFinalizing) return 'Finalizing...';
+ if (ghLoginIsBusy) return 'Waiting...';
+ return 'Start login';
+ })();
+ const ghLoginInlineRunDisabled = !ghInstalled || ghLoggedIn || ghLoginIsBusy;
+ const showInlineGhLogin = currentId === 'install-gh' && ghInstalled;
+ const isGhLoginStep = currentId === 'gh-login';
+ const codexNeedsNode = currentId === 'install-codex' && !(toolsMap.get('node') && toolsMap.get('npm'));
+ const gitIdentityName = this.escapeHtml(String(state.gitIdentity?.name || ''));
+ const gitIdentityEmail = this.escapeHtml(String(state.gitIdentity?.email || ''));
+ const showRunButton = current?.runSupported !== false && !isGitIdentityStep && !(isGhLoginStep && current?.done);
+ const runDisabled = !!current?.done || runStatus === 'verified' || isRunBusy || codexNeedsNode;
+ const runLabel = (() => {
+ if (current?.done || runStatus === 'verified') {
+ if (currentId === 'gh-login') return 'Logged in';
+ if (currentId === 'configure-git-identity') return 'Configured';
+ return 'Installed';
+ }
+ if (isFinalizing) return 'Finalizing...';
+ if (isRunBusy) return isGhLoginStep ? 'Waiting...' : 'Running...';
+ if (currentId === 'gh-login') return 'Start login';
+ return 'Run step';
+ })();
+ const baseStatusText = String(current?.statusText || (current?.done ? 'Installed' : 'Missing'));
+ const statusText = (() => {
+ if (runStatus === 'verified') return baseStatusText;
+ if (isFinalizing) return isGhLoginStep ? 'Finalizing login' : 'Finalizing';
+ if (isRunning) return isGhLoginStep ? 'Signing in' : (isGitIdentityStep ? 'Saving' : 'Installing');
+ if (isVerifying) return isGhLoginStep ? 'Checking login' : (isGitIdentityStep ? 'Checking' : 'Verifying');
+ if (runStatus === 'failed') return isGhLoginStep ? 'Login failed' : (isGitIdentityStep ? 'Save failed' : 'Failed');
+ return baseStatusText;
+ })();
+ const statusClass = current?.done || runStatus === 'verified'
+ ? 'status-ok'
+ : ((isRunning || isVerifying || isFinalizing) ? 'status-pending' : (runStatus === 'failed' ? 'status-missing' : (current?.statusClass || 'status-missing')));
+ const canAdvance = !!current?.done || !!current?.optional;
+ const nextLabel = !canAdvance
+ ? 'Complete this step first'
+ : (!current?.done && current?.optional
+ ? 'Skip'
+ : (stepNo >= totalSteps ? 'Finish onboarding' : 'Next step'));
+
+ listEl.innerHTML = `
+
+ ${steps.map((step, idx) => {
+ const isActive = idx === state.currentStep;
+ const actionId = String(step?.id || '').trim();
+ const isSkipped = state.skippedActionIds.has(actionId);
+ const isDone = step.done || isSkipped;
+ const isPast = idx < state.currentStep;
+ const isFuture = idx > state.currentStep;
+ let statusClass = 'stepper-upcoming';
+ if (isActive) {
+ statusClass = 'stepper-active';
+ } else if (isPast && isDone) {
+ statusClass = 'stepper-done';
+ } else {
+ statusClass = 'stepper-upcoming';
+ }
+ const stepStateLabel = isActive
+ ? 'Current step'
+ : (isPast && isDone ? 'Completed' : (isFuture ? 'Upcoming' : 'Pending'));
+ return `
+
+
+ ${isActive ? `
Step ${stepNo}` : ''}
+
+
+
+ `;
+ }).join('')}
+
+
+
+
+ ${currentStepIconSvg}
+
+
+
${currentTitle}
+
+
+ ${current?.done ? '
' : ''}
+
${currentDesc} ${statusText ? `(${statusText})` : ''}
+
+
+ ${isGitIdentityStep ? `
+
+ ` : ''}
+
+ ${showInlineGhLogin ? `
+
+
GitHub authentication (optional) (${this.escapeHtml(ghLoginInlineStatusText)})
+ ${ghLoginUiPhase === 'start' ? '
Click Start login to begin browser sign-in.
' : ''}
+ ${ghLoginUiPhase === 'wait-code' ? '
Waiting for GitHub CLI login details. If code is not shown here, it is copied to your clipboard automatically.
' : ''}
+ ${ghLoginUiPhase === 'retry' ? '
Login is not complete yet. Start login again to request a new one-time code.
' : ''}
+ ${ghLoginUiPhase === 'code' ? `
Open GitHub login and paste this one-time code.
${this.escapeHtml(ghLoginCode)}
` : ''}
+
+
+ ${ghLoginRunInfo ? `` : ''}
+
+
+ ` : ''}
+
+ ${shouldShowInstallerOutput ? `
+
+
${installerOutputText}
+
+ ` : ''}
+
+
+ ${showRunButton ? `` : ''}
+ ${!isGhLoginStep && !isGitIdentityStep ? `` : ''}
+
+
+
+
+
+
+
+
`;
+
+ return { req, steps, current };
+ };
+
+ const closeModal = ({ force = false } = {}) => {
+ const locked = applyOnboardingLockUI();
+ if (!force && locked) {
+ openModal();
+ return false;
+ }
+ if (!force && !readCompleted()) {
+ writeDismissed(true);
+ }
+ modal.classList.add('hidden');
+ body?.classList?.remove?.('dependency-onboarding-active');
+ setBootstrapPending(false);
+ return true;
+ };
+ const openModal = ({ showWelcome = null, allowDuringStartup = false } = {}) => {
+ if (readCompleted()) {
+ closeModal({ force: true });
+ return false;
+ }
+ if (state.startupPending && !allowDuringStartup) {
+ return false;
+ }
+ const wasHidden = modal.classList.contains('hidden');
+ modal.classList.remove('hidden');
+ state.startupPending = false;
+ setBootstrapPending(false);
+ body?.classList?.add?.('dependency-onboarding-active');
+ if (typeof showWelcome === 'boolean') {
+ state.showWelcome = showWelcome;
+ } else if (wasHidden) {
+ state.showWelcome = true;
+ }
+ if (state.diagnostics && Array.isArray(state.actions) && state.actions.length > 0) {
+ render();
+ }
+ applyOnboardingLockUI();
+ return true;
+ };
+
+ const setLoading = (loading) => {
+ state.loading = !!loading;
+ if (openBtn) openBtn.disabled = state.loading;
+ if (state.loading) {
+ summaryEl.textContent = '';
+ }
+ };
+
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
+
+ const loadAndRender = async ({ open = false, forceAutoShow = false, bootstrap = false, explicitOpen = false } = {}) => {
+ if (state.loading) return false;
+ setLoading(true);
+ try {
+ const [persisted, diagRes, actionsRes] = await Promise.all([
+ loadPersistedSetupState(),
+ fetch('/api/diagnostics'),
+ fetch('/api/setup-actions')
+ ]);
+ const diagData = await diagRes.json().catch(() => ({}));
+ const actionsData = await actionsRes.json().catch(() => ({}));
+
+ if (!diagRes.ok || diagData?.ok === false) {
+ throw new Error(String(diagData?.error || `Diagnostics HTTP ${diagRes.status}`));
+ }
+ if (!actionsRes.ok || actionsData?.ok === false) {
+ throw new Error(String(actionsData?.error || `Setup actions HTTP ${actionsRes.status}`));
+ }
+
+ state.diagnostics = diagData;
+ hydrateGitIdentityDraft(diagData);
+ const allActions = Array.isArray(actionsData?.actions) ? actionsData.actions : [];
+ const toolsMap = toToolMap(diagData);
+ state.actions = allActions.filter((action) => String(action?.id || '').trim() !== 'gh-login');
+ const allowedActionIds = new Set(
+ state.actions
+ .map((action) => String(action?.id || '').trim())
+ .filter(Boolean)
+ );
+ const persistedSkippedIds = new Set(
+ (Array.isArray(persisted?.skippedActionIds) ? persisted.skippedActionIds : [])
+ .filter((id) => allowedActionIds.has(String(id || '').trim()))
+ );
+ state.skippedActionIds = new Set(
+ Array.from(persistedSkippedIds)
+ );
+ if (state.actions.length > 0) {
+ const savedStep = Math.max(0, Number(persisted?.currentStep) || 0);
+ setCurrentStep(savedStep, { persist: false });
+ }
+ const view = render();
+ applyOnboardingLockUI();
+
+ const hasCompletedOnboarding = readCompleted();
+ const coreReady = !!view.req?.coreReady;
+ if (hasCompletedOnboarding) {
+ state.startupPending = false;
+ closeModal({ force: true });
+ }
+ const shouldAutoShow = isWindowsHost && !hasCompletedOnboarding && (forceAutoShow || !readDismissed());
+ const shouldKeepVisible = !hasCompletedOnboarding && open && !modal.classList.contains('hidden');
+ if (explicitOpen || shouldKeepVisible || shouldAutoShow) {
+ openModal({ allowDuringStartup: bootstrap || explicitOpen });
+ } else {
+ state.startupPending = false;
+ setBootstrapPending(false);
+ }
+ return true;
+ } catch (err) {
+ summaryEl.textContent = `Dependency check failed: ${String(err?.message || err)}`;
+ listEl.innerHTML = '
Unable to load setup actions right now.
';
+ const shouldOpenOnError = explicitOpen || (open && !modal.classList.contains('hidden'));
+ if (shouldOpenOnError) openModal({ allowDuringStartup: bootstrap || explicitOpen });
+ else if (!bootstrap) setBootstrapPending(false);
+ return false;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const stopRunPolling = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ const poller = state.actionRunPollers.get(id);
+ if (poller?.timer) clearTimeout(poller.timer);
+ state.actionRunPollers.delete(id);
+ };
+
+ const updateActionRunState = (actionId, patch = {}, { rerender = true } = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ const prev = state.actionRuns.get(id) || { actionId: id };
+ const next = {
+ ...prev,
+ ...patch,
+ actionId: id
+ };
+ state.actionRuns.set(id, next);
+ if (rerender) render();
+ return next;
+ };
+
+ const fetchSetupActionRunStatus = async ({ runId = '', actionId = '' } = {}) => {
+ const params = new URLSearchParams();
+ if (runId) params.set('runId', String(runId));
+ if (actionId) params.set('actionId', String(actionId));
+ const res = await fetch(`/api/setup-actions/run-status?${params.toString()}`);
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false || !data?.run) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
+ }
+ return data.run;
+ };
+
+ const getVerifyPolicyForAction = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (id === 'gh-login') {
+ return { attempts: 14, delayMs: 900 };
+ }
+ if (id === 'install-git' || id === 'install-node' || id === 'install-gh') {
+ return { attempts: 10, delayMs: 650 };
+ }
+ return { attempts: 8, delayMs: 650 };
+ };
+
+ const verifyActionInstalled = async (actionId, runId, options = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return false;
+ const policy = {
+ ...getVerifyPolicyForAction(id),
+ ...(options && typeof options === 'object' ? options : {})
+ };
+ const attempts = Math.max(1, Number(policy.attempts) || 1);
+ const delayMs = Math.max(250, Number(policy.delayMs) || 650);
+
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ const runState = state.actionRuns.get(id);
+ if (!runState || String(runState?.runId || '') !== String(runId || '')) return false;
+
+ updateActionRunState(id, {
+ status: 'verifying',
+ verifyAttempt: attempt,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+
+ await loadAndRender({ open: true, forceAutoShow: true });
+ const toolsMap = toToolMap(state.diagnostics);
+ if (isActionComplete(id, toolsMap)) {
+ updateActionRunState(id, {
+ status: 'verified',
+ verifyAttempt: attempts,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Dependency detected automatically.', 'success');
+ return true;
+ }
+ await sleep(delayMs);
+ }
+
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(
+ id === 'gh-login'
+ ? 'GitHub login is not detected yet. Complete sign-in in browser and try again.'
+ : 'Install finished but dependency is still missing. Review output and run again if needed.',
+ 'warning'
+ );
+ return false;
+ };
+
+ const verifyActionWithoutRun = async (actionId, options = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return false;
+ const policy = {
+ ...getVerifyPolicyForAction(id),
+ ...(options && typeof options === 'object' ? options : {})
+ };
+ const attempts = Math.max(1, Number(policy.attempts) || 1);
+ const delayMs = Math.max(250, Number(policy.delayMs) || 650);
+
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ updateActionRunState(id, {
+ status: 'verifying',
+ verifyAttempt: attempt,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+
+ await loadAndRender({ open: true, forceAutoShow: true });
+ const toolsMap = toToolMap(state.diagnostics);
+ if (isActionComplete(id, toolsMap)) {
+ updateActionRunState(id, {
+ status: 'verified',
+ verifyAttempt: attempts,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+ return true;
+ }
+ if (attempt < attempts) {
+ await sleep(delayMs);
+ }
+ }
+
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ updatedAt: new Date().toISOString()
+ });
+ return false;
+ };
+
+ const pollRunUntilDone = async (actionId, runId) => {
+ const id = String(actionId || '').trim();
+ const rid = String(runId || '').trim();
+ if (!id || !rid) return;
+
+ stopRunPolling(id);
+ const pollLoop = async () => {
+ try {
+ const run = await fetchSetupActionRunStatus({ runId: rid, actionId: id });
+ updateActionRunState(id, run);
+
+ if (String(run?.status || '').toLowerCase() === 'running') {
+ const timer = setTimeout(pollLoop, 850);
+ state.actionRunPollers.set(id, { runId: rid, timer });
+ return;
+ }
+
+ stopRunPolling(id);
+ await loadAndRender({ open: true, forceAutoShow: true });
+
+ if (String(run?.status || '').toLowerCase() === 'success') {
+ await verifyActionInstalled(id, rid);
+ return;
+ }
+
+ if (String(run?.status || '').toLowerCase() === 'failed') {
+ if (id === 'gh-login') {
+ const ghRunLines = collectRunOutputLines(run, { limit: 160 });
+ const ghRunInfo = extractGithubLoginInfo(ghRunLines);
+ const hasDeviceSignal = ghRunInfo.sawDeviceHint || !!ghRunInfo.code || /login\/device/i.test(ghRunLines.join('\n'));
+ const verifyOptions = hasDeviceSignal
+ ? { attempts: 14, delayMs: 900 }
+ : { attempts: 5, delayMs: 700 };
+ const detected = await verifyActionWithoutRun(id, verifyOptions);
+ if (detected) {
+ this.showToast('GitHub login detected automatically.', 'success');
+ return;
+ }
+
+ const runError = String(run?.error || '').trim();
+ const exitCode = Number(run?.exitCode);
+ const interrupted = exitCode === 1 || /code\s*1/i.test(runError) || /cancel|not completed/i.test(runError);
+ const missingDeviceCode = !hasDeviceSignal;
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: interrupted
+ ? 'Login was not completed in browser.'
+ : (missingDeviceCode
+ ? 'GitHub CLI did not return a one-time code.'
+ : (runError || 'Login was not completed.')),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(
+ missingDeviceCode
+ ? 'GitHub CLI did not return a one-time code. Click Start login again.'
+ : (interrupted
+ ? 'GitHub login is still not detected. If browser sign-in just finished, click Start login again.'
+ : `GitHub login failed: ${runError || 'Unknown error'}`),
+ (interrupted || missingDeviceCode) ? 'warning' : 'error'
+ );
+ return;
+ }
+ this.showToast(`Install failed: ${String(run?.error || 'Unknown error')}`, 'error');
+ }
+ } catch (err) {
+ stopRunPolling(id);
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err || 'Failed to fetch setup status'),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Install monitoring failed: ${String(err?.message || err)}`, 'error');
+ }
+ };
+
+ const timer = setTimeout(pollLoop, 250);
+ state.actionRunPollers.set(id, { runId: rid, timer });
+ };
+
+ const runBootstrapLoad = async () => {
+ state.startupPending = true;
+ setBootstrapPending(true);
+ const delaysMs = [0, 240, 420, 700, 1050, 1450, 1900];
+ for (let attempt = 0; attempt < delaysMs.length; attempt += 1) {
+ if (attempt > 0) {
+ await sleep(delaysMs[attempt]);
+ }
+ const ok = await loadAndRender({ open: false, forceAutoShow: false, bootstrap: true });
+ if (ok) return;
+ }
+ state.startupPending = false;
+ setBootstrapPending(false);
+ };
+
+ const runSetupAction = async (actionId, btnEl) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ const button = btnEl || null;
+ if (button) button.disabled = true;
+ try {
+ const existingRunStatus = String(state.actionRuns.get(id)?.status || '').trim().toLowerCase();
+ if (existingRunStatus === 'running' || existingRunStatus === 'verifying' || existingRunStatus === 'success' || existingRunStatus === 'completed') {
+ this.showToast(
+ id === 'gh-login'
+ ? 'Login is still in progress. Please wait while we finish checking.'
+ : 'Install is still in progress. Please wait while we finish checking.',
+ 'info'
+ );
+ return;
+ }
+
+ const existingStep = getResolvedSteps().find((step) => String(step?.id || '').trim() === id);
+ const toolsMap = toToolMap(state.diagnostics);
+ if (id === 'gh-login' && !toToolMap(state.diagnostics).get('gh')) {
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: 'Install GitHub CLI before starting login.',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Install GitHub CLI first. Login is optional and only available after installation.', 'warning');
+ await loadAndRender({ open: true, forceAutoShow: true });
+ return;
+ }
+ if (id === 'install-codex' && !(toolsMap.get('node') && toolsMap.get('npm'))) {
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: 'Install Node.js LTS first. Codex requires npm.',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Install Node.js LTS first. Codex depends on npm.', 'warning');
+ await loadAndRender({ open: true, forceAutoShow: true });
+ return;
+ }
+ if (existingStep?.done) {
+ updateActionRunState(id, {
+ status: 'verified',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Dependency already detected.', 'success');
+ await loadAndRender({ open: true, forceAutoShow: true });
+ return;
+ }
+
+ updateActionRunState(id, {
+ runId: null,
+ status: 'running',
+ error: null,
+ output: [],
+ verifyAttempt: 0,
+ verifyMax: 0,
+ updatedAt: new Date().toISOString()
+ });
+ const res = await fetch('/api/setup-actions/run', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ actionId: id })
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
}
- if (!state.base) await refreshBase();
- await refreshInstallWizard().catch(() => {});
- render(state.base, state.firstRun, state.wizard);
-
- const appliedCount = Number(data?.appliedCount || 0);
- const failedCount = Number(data?.failedCount || 0);
- const skippedManualCount = Number(data?.skippedManualCount || 0);
- if (failedCount > 0) {
- this.showToast?.(`Auto-fix applied ${appliedCount}, failed ${failedCount}`, 'warning');
+ const run = (data?.run && typeof data.run === 'object') ? data.run : null;
+ if (run) {
+ updateActionRunState(id, {
+ ...run,
+ verifyAttempt: 0,
+ verifyMax: 0
+ });
+ if (run?.runId) {
+ await pollRunUntilDone(id, run.runId);
+ } else {
+ await loadAndRender({ open: true, forceAutoShow: true });
+ }
} else {
- const tail = skippedManualCount > 0 ? `, ${skippedManualCount} manual step(s) left` : '';
- this.showToast?.(`Auto-fix applied ${appliedCount}${tail}`, 'success');
+ await loadAndRender({ open: true, forceAutoShow: true });
}
- if (statusEl) statusEl.textContent = 'Safe auto-fix completed';
- } catch (error) {
- this.showToast?.(`Safe auto-fix failed: ${String(error?.message || error)}`, 'error');
- if (statusEl) statusEl.textContent = '';
+ const defaultMessage = data?.alreadyRunning
+ ? (id === 'gh-login'
+ ? 'GitHub login is already running. Complete it in your browser.'
+ : 'Install is already running. Watching for completion...')
+ : (id === 'gh-login'
+ ? 'GitHub login started. Complete sign-in in your browser.'
+ : 'Install started. We will check this step automatically.');
+ this.showToast(String(data?.message || defaultMessage), 'info');
+ } catch (err) {
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Failed to start action: ${String(err?.message || err)}`, 'error');
} finally {
- btnRepairSafe.disabled = false;
+ if (button) button.disabled = false;
}
- });
- repairEl?.addEventListener('click', async (event) => {
- const target = event.target.closest('[data-diagnostics-repair]');
- if (!target) return;
- const action = String(target.getAttribute('data-diagnostics-repair') || '').trim();
- if (!action) return;
- target.disabled = true;
- if (statusEl) statusEl.textContent = `Running repair: ${action}โฆ`;
+ };
+
+ const saveGitIdentity = async (btnEl) => {
+ const button = btnEl || null;
+ const id = 'configure-git-identity';
+ const nameInput = listEl.querySelector('[data-setup-git-name]');
+ const emailInput = listEl.querySelector('[data-setup-git-email]');
+ const name = String(nameInput?.value || state.gitIdentity?.name || '').trim();
+ const email = String(emailInput?.value || state.gitIdentity?.email || '').trim();
+
+ state.gitIdentity.name = name;
+ state.gitIdentity.email = email;
+
+ if (!name || !email) {
+ this.showToast('Enter both Git name and email.', 'warning');
+ return;
+ }
+
+ if (button) button.disabled = true;
try {
- const res = await fetch('/api/diagnostics/first-run/repair', {
+ updateActionRunState(id, {
+ runId: 'manual-git-identity',
+ status: 'running',
+ error: null,
+ output: [],
+ verifyAttempt: 0,
+ verifyMax: 0,
+ updatedAt: new Date().toISOString()
+ });
+
+ const res = await fetch('/api/setup-actions/configure-git-identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action })
+ body: JSON.stringify({ name, email })
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.ok === false) {
- throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`));
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
}
- const repair = data?.repair || {};
- if (repair.manual) {
- this.showToast?.(String(repair?.message || 'Manual action required'), 'warning');
+
+ state.gitIdentity.name = String(data?.name || name).trim();
+ state.gitIdentity.email = String(data?.email || email).trim();
+
+ const detected = await verifyActionWithoutRun(id, { attempts: 6, delayMs: 350 });
+ if (detected) {
+ this.showToast('Git identity saved and detected automatically.', 'success');
} else {
- this.showToast?.(String(repair?.message || 'Repair completed'), 'success');
+ this.showToast('Git identity saved, but detection is delayed. Try saving again in a few seconds.', 'warning');
}
- if (data?.diagnostics) {
- state.firstRun = data.diagnostics;
- renderRepairActions(state.firstRun);
- } else {
- await refreshFirstRun();
- }
- if (!state.base) await refreshBase();
- await refreshInstallWizard().catch(() => {});
- render(state.base, state.firstRun, state.wizard);
- if (statusEl) statusEl.textContent = `Repair completed: ${action}`;
- } catch (error) {
- this.showToast?.(`Repair failed: ${String(error?.message || error)}`, 'error');
- if (statusEl) statusEl.textContent = '';
+ } catch (err) {
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Failed to save Git identity: ${String(err?.message || err)}`, 'error');
} finally {
- target.disabled = false;
+ if (button) button.disabled = false;
+ await loadAndRender({ open: true, forceAutoShow: true });
+ }
+ };
+
+ listEl.addEventListener('click', async (event) => {
+ const runBtn = event.target.closest('[data-setup-run]');
+ if (runBtn) {
+ await runSetupAction(runBtn.getAttribute('data-setup-run'), runBtn);
+ return;
+ }
+
+ const saveGitBtn = event.target.closest('[data-setup-git-save]');
+ if (saveGitBtn) {
+ await saveGitIdentity(saveGitBtn);
+ return;
+ }
+
+ const prevBtn = event.target.closest('[data-setup-prev]');
+ if (prevBtn) {
+ setCurrentStep(state.currentStep - 1);
+ render();
+ return;
+ }
+
+ const nextBtn = event.target.closest('[data-setup-next]');
+ if (nextBtn) {
+ const total = Array.isArray(state.actions) ? state.actions.length : 0;
+ const steps = getResolvedSteps();
+ syncSkippedSteps(steps);
+ const currentStep = steps[state.currentStep];
+ if (!currentStep?.done) {
+ if (!currentStep?.optional) {
+ this.showToast('Install this dependency before continuing.', 'warning');
+ return;
+ }
+ setStepSkipped(currentStep?.id, true);
+ this.showToast('Skipping optional setup for now. You can configure it later.', 'warning');
+ } else {
+ setStepSkipped(currentStep?.id, false);
+ }
+ if (state.currentStep >= (total - 1)) {
+ writeCompleted(true);
+ writeDismissed(false);
+ closeModal({ force: true });
+ this.showToast('Dependency onboarding complete.', 'success');
+ return;
+ }
+ setCurrentStep(state.currentStep + 1);
+ render();
+ return;
+ }
+ const beginBtn = event.target.closest('[data-setup-begin]');
+ if (beginBtn) {
+ state.showWelcome = false;
+ render();
+ return;
+ }
+
+ const jumpBtn = event.target.closest('[data-setup-jump]');
+ if (jumpBtn) {
+ const idx = Number.parseInt(String(jumpBtn.getAttribute('data-setup-jump') || ''), 10);
+ if (Number.isFinite(idx)) {
+ setCurrentStep(idx);
+ render();
+ }
+ return;
+ }
+
+ const copyBtn = event.target.closest('[data-setup-copy-id]');
+ if (copyBtn) {
+ const actionId = String(copyBtn.getAttribute('data-setup-copy-id') || '').trim();
+ const action = (Array.isArray(state.actions) ? state.actions : []).find((item) => String(item?.id || '').trim() === actionId);
+ const command = String(action?.command || '').trim();
+ if (!command) return;
+ try {
+ await navigator.clipboard.writeText(command);
+ this.showToast('Command copied to clipboard.', 'success');
+ } catch (err) {
+ this.showToast(`Copy failed: ${String(err?.message || err)}`, 'error');
+ }
+ return;
+ }
+
+ const copyGhCodeBtn = event.target.closest('[data-setup-copy-gh-code]');
+ if (copyGhCodeBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ const code = String(copyGhCodeBtn.getAttribute('data-setup-copy-gh-code') || '').trim();
+ if (!code) return;
+ try {
+ await navigator.clipboard.writeText(code);
+ this.showToast('GitHub one-time code copied.', 'success');
+ } catch (err) {
+ this.showToast(`Copy failed: ${String(err?.message || err)}`, 'error');
+ }
+ return;
+ }
+
+ const openGhLoginBtn = event.target.closest('[data-setup-open-gh-login]');
+ if (openGhLoginBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ const link = String(openGhLoginBtn.getAttribute('data-setup-open-gh-login') || '').trim();
+ if (!link) return;
+ try {
+ const res = await fetch('/api/setup-actions/open-url', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: link })
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
+ }
+ this.showToast('Opened GitHub login in your browser.', 'info');
+ } catch (err) {
+ this.showToast(`Could not open login link: ${String(err?.message || err)}`, 'error');
+ }
+ return;
+ }
+
+ const toggleGitHelpBtn = event.target.closest('[data-setup-toggle-git-help]');
+ if (toggleGitHelpBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ state.gitIdentityHelpVisible = !state.gitIdentityHelpVisible;
+ render();
+ return;
}
});
- }
+
+ if (openBtn) {
+ openBtn.addEventListener('click', () => {
+ writeDismissed(false);
+ setCurrentStep(0);
+ loadAndRender({ open: true, forceAutoShow: true, explicitOpen: true });
+ });
+ }
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => closeModal());
+ }
+
+ modal.addEventListener('click', (event) => {
+ if (event.target === modal) closeModal();
+ });
+
+ document.addEventListener('keydown', (event) => {
+ if (event.key !== 'Escape') return;
+ if (modal.classList.contains('hidden')) return;
+ closeModal();
+ });
+
+ runBootstrapLoad();
+ }
notifyWorkflow({ type = 'info', message = '', sessionId = null, metadata = null } = {}) {
const msg = String(message || '').trim();
@@ -9353,46 +10761,59 @@ class ClaudeOrchestrator {
}
}
}
-
- showToast(message, type = 'info') {
+
+ showToast(message, type = 'info', options = {}) {
+ const rawMessage = String(message || '').trim();
+ if (!rawMessage) return;
+
+ const normalizedType = (['info', 'success', 'warning', 'error'].includes(type)) ? type : 'info';
+ const durationMsRaw = Number(options?.durationMs);
+ const durationMs = Number.isFinite(durationMsRaw) ? Math.max(1200, durationMsRaw) : 5000;
+
+ let stack = document.getElementById('toast-stack');
+ if (!stack) {
+ stack = document.createElement('div');
+ stack.id = 'toast-stack';
+ stack.className = 'toast-stack';
+ document.body.appendChild(stack);
+ }
+
+ const iconByType = {
+ info: '
',
+ success: '
',
+ warning: '
',
+ error: '
'
+ };
+
const toast = document.createElement('div');
- toast.className = `toast toast-${type}`;
+ toast.className = `toast toast-${normalizedType}`;
+ toast.setAttribute('role', 'status');
toast.innerHTML = `
- ${type === 'success' ? 'โ
' : type === 'warning' ? 'โ ๏ธ' : type === 'error' ? 'โ' : 'โน๏ธ'}
- ${message}
+ ${iconByType[normalizedType] || iconByType.info}
+ ${this.escapeHtml(rawMessage)}
+
`;
-
- // Add styles for different toast types
- const styles = {
- info: 'var(--accent-primary)',
- success: 'var(--accent-success)',
- warning: 'var(--accent-warning)',
- error: 'var(--accent-danger)'
- };
-
- toast.style.cssText = `
- position: fixed;
- top: calc(var(--header-height) + 20px);
- right: 20px;
- background: ${styles[type]};
- color: white;
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-md);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- animation: slideInRight 0.3s ease-out, fadeOutRight 0.3s ease-in 4.7s forwards;
- `;
-
- document.body.appendChild(toast);
-
- // Remove after 5 seconds
- setTimeout(() => {
- if (toast.parentNode) {
+
+ const removeToast = () => {
+ if (!toast.parentNode) return;
+ if (toast.classList.contains('is-leaving')) return;
+ toast.classList.add('is-leaving');
+ setTimeout(() => {
toast.remove();
- }
- }, 5000);
+ if (stack && !stack.children.length) {
+ stack.remove();
+ }
+ }, 240);
+ };
+
+ const closeBtn = toast.querySelector('.toast-close');
+ closeBtn?.addEventListener('click', removeToast);
+
+ stack.appendChild(toast);
+ requestAnimationFrame(() => toast.classList.add('is-visible'));
+ setTimeout(removeToast, durationMs);
}
async launchDiffViewer(githubUrl) {
@@ -9400,9 +10821,9 @@ class ClaudeOrchestrator {
const prMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/);
const commitMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/commit\/([a-f0-9]{40})/);
const compareMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/compare\/([^?#]+)/);
-
+
let diffViewerPath = '';
-
+
if (prMatch) {
const [, owner, repo, pr] = prMatch;
diffViewerPath = `/pr/${owner}/${repo}/${pr}`;
@@ -9430,7 +10851,7 @@ class ClaudeOrchestrator {
this.showToast('Unable to parse GitHub URL', 'error');
return;
}
-
+
// Open a placeholder tab immediately (avoids popup blockers), then redirect once ready.
const popup = window.open('', '_blank');
if (!popup) {
@@ -12743,19 +14164,7 @@ class ClaudeOrchestrator {
});
bodyEl.querySelectorAll?.('[data-open-diagnostics="true"]')?.forEach?.((btn) => {
btn.addEventListener('click', () => {
- try {
- document.getElementById('settings-panel')?.classList?.remove?.('hidden');
- setTimeout(() => {
- try {
- document.getElementById('diagnostics-output')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
- } catch {}
- try {
- document.getElementById('diagnostics-refresh')?.click?.();
- } catch {}
- }, 50);
- } catch {
- // ignore
- }
+ this.openDiagnosticsPanel({ refresh: true });
});
});
bodyEl.querySelectorAll('[data-pr-refresh]').forEach((btn0) => {
@@ -12899,7 +14308,7 @@ class ClaudeOrchestrator {
// Check URL params first
const urlParams = new URLSearchParams(window.location.search);
const tokenFromUrl = urlParams.get('token');
-
+
if (tokenFromUrl) {
// Save to localStorage for future use
localStorage.setItem('claude-orchestrator-token', tokenFromUrl);
@@ -12907,7 +14316,7 @@ class ClaudeOrchestrator {
window.history.replaceState({}, document.title, window.location.pathname);
return tokenFromUrl;
}
-
+
// Check localStorage
return localStorage.getItem('claude-orchestrator-token');
}
@@ -13733,7 +15142,7 @@ class ClaudeOrchestrator {
this.showToast?.(`Pruned ${prunedCount} old recoverable session(s)`, 'success');
return true;
}
-
+
installAuthFetchShim() {
if (window.__claudeOrchestratorFetchAuthInstalled) return;
if (typeof window.fetch !== 'function') return;
@@ -13777,7 +15186,7 @@ class ClaudeOrchestrator {
window.__claudeOrchestratorFetchAuthInstalled = true;
}
-
+
// Terminal Focus Feature - Now shows only that worktree
focusTerminal(sessionId) {
// Extract worktree ID from session ID
@@ -13815,14 +15224,14 @@ class ClaudeOrchestrator {
console.error(`Terminal instance not found for ${sessionId}`);
return;
}
-
+
// Store original parent for unfocus
const terminalElement = terminalWrapper.querySelector('.terminal');
if (!terminalElement) {
console.error(`Terminal element not found in wrapper for ${sessionId}`);
return;
}
-
+
this.focusedTerminalInfo = {
sessionId: sessionId,
originalParent: terminalElement.parentElement,
@@ -13834,41 +15243,41 @@ class ClaudeOrchestrator {
rows: xtermInstance.rows || 24
}
};
-
+
// Add focusing animation to original terminal
terminalWrapper.classList.add('focusing');
-
+
// Update overlay header
const focusedTitle = document.getElementById('focused-title');
const focusedBranch = document.getElementById('focused-branch');
const focusedStatus = document.getElementById('focused-status');
-
+
const isAgentSession = /-(claude|codex)$/.test(String(sessionId || ''));
const worktreeNumber = sessionId.split('-')[0].replace('work', '');
-
+
if (focusedTitle) focusedTitle.textContent = `${isAgentSession ? '๐ค Agent' : '๐ป Server'} ${worktreeNumber}`;
if (focusedBranch) focusedBranch.textContent = this.formatBranchLabel(session.branch || '', { context: 'terminal' }).text || '';
if (focusedStatus) focusedStatus.className = `status-indicator ${session.status || 'idle'}`;
-
+
// Move the actual terminal element to focused container
const focusedTerminalBody = document.getElementById('focused-terminal-body');
if (!focusedTerminalBody) {
console.error('Focused terminal body container not found');
return;
}
-
+
focusedTerminalBody.innerHTML = '';
focusedTerminalBody.appendChild(terminalElement);
-
+
// Hide original wrapper
terminalWrapper.style.visibility = 'hidden';
-
+
// Activate focus overlay with animation
const focusOverlay = document.getElementById('focus-overlay');
if (focusOverlay) {
focusOverlay.classList.add('active');
}
-
+
// Bind ESC key for unfocus
this.handleEscKey = (e) => {
if (e.key === 'Escape') {
@@ -13876,41 +15285,41 @@ class ClaudeOrchestrator {
}
};
document.addEventListener('keydown', this.handleEscKey);
-
+
// Resize terminal to fit the focused container after animation
setTimeout(() => {
try {
// Store original font size
this.focusedTerminalInfo.originalFontSize = xtermInstance.options.fontSize || 12;
-
+
// Increase font size for better readability in focused mode
const originalSize = this.focusedTerminalInfo.originalFontSize;
const newFontSize = Math.round(originalSize * 1.8); // 1.8x larger (reduced from 3x by ~60%)
xtermInstance.options.fontSize = newFontSize;
-
+
const rect = focusedTerminalBody.getBoundingClientRect();
// Calculate new dimensions based on container size with larger font
const charWidth = newFontSize * 0.6; // Approximate character width
const lineHeight = newFontSize * 1.4; // Approximate line height
-
+
const cols = Math.floor((rect.width - 30) / charWidth);
const rows = Math.floor((rect.height - 30) / lineHeight);
-
+
// Apply reasonable limits
const finalCols = Math.min(200, Math.max(80, cols));
const finalRows = Math.min(80, Math.max(24, rows));
-
+
console.log(`Resizing focused terminal from ${xtermInstance.cols}x${xtermInstance.rows} to ${finalCols}x${finalRows} with font size ${newFontSize}px`);
-
+
// Resize xterm
xtermInstance.resize(finalCols, finalRows);
-
+
// Use fit addon if available
const fitAddon = this.terminalManager?.fitAddons?.get(sessionId);
if (fitAddon) {
fitAddon.fit();
}
-
+
// Send resize command to backend
if (this.socket) {
this.socket.emit('resize', {
@@ -13919,46 +15328,46 @@ class ClaudeOrchestrator {
rows: finalRows
});
}
-
+
// Focus the terminal for input
xtermInstance.focus();
} catch (resizeError) {
console.error('Error resizing focused terminal:', resizeError);
}
}, 200);
-
+
// Remove focusing animation after transition
setTimeout(() => {
terminalWrapper.classList.remove('focusing');
}, 300);
-
+
} catch (error) {
console.error('Error focusing terminal:', error);
}
}
-
+
unfocusTerminal() {
try {
if (!this.focusedTerminalInfo) return;
-
+
const { sessionId, originalParent, originalNextSibling, terminalElement, terminalWrapper, originalDimensions } = this.focusedTerminalInfo;
-
+
// Move terminal element back to original location
if (originalNextSibling) {
originalParent.insertBefore(terminalElement, originalNextSibling);
} else {
originalParent.appendChild(terminalElement);
}
-
+
// Show original wrapper
terminalWrapper.style.visibility = 'visible';
-
+
// Deactivate focus overlay
const focusOverlay = document.getElementById('focus-overlay');
if (focusOverlay) {
focusOverlay.classList.remove('active');
}
-
+
// Restore original terminal size and font
const xtermInstance = this.terminalManager?.terminals?.get(sessionId);
if (xtermInstance) {
@@ -13966,21 +15375,21 @@ class ClaudeOrchestrator {
const originalFontSize = this.focusedTerminalInfo.originalFontSize || 12;
console.log(`Restoring font size from ${xtermInstance.options.fontSize}px to ${originalFontSize}px`);
xtermInstance.options.fontSize = originalFontSize;
-
+
// Force a refresh of the terminal to apply font change
xtermInstance.refresh(0, xtermInstance.rows - 1);
-
+
if (originalDimensions) {
setTimeout(() => {
console.log(`Restoring terminal dimensions to ${originalDimensions.cols}x${originalDimensions.rows}`);
xtermInstance.resize(originalDimensions.cols, originalDimensions.rows);
-
+
// Use fit addon if available
const fitAddon = this.terminalManager?.fitAddons?.get(sessionId);
if (fitAddon) {
setTimeout(() => fitAddon.fit(), 50);
}
-
+
// Send resize command to backend
if (this.socket) {
this.socket.emit('resize', {
@@ -13992,10 +15401,10 @@ class ClaudeOrchestrator {
}, 100);
}
}
-
+
// Clean up
this.focusedTerminalInfo = null;
-
+
// Remove ESC key listener
if (this.handleEscKey) {
document.removeEventListener('keydown', this.handleEscKey);
@@ -14005,14 +15414,14 @@ class ClaudeOrchestrator {
console.error('Error unfocusing terminal:', error);
}
}
-
+
calculateTerminalDimensions(container) {
if (!container) return null;
-
+
const rect = container.getBoundingClientRect();
const cols = Math.floor(rect.width / 9); // Approximate character width
const rows = Math.floor(rect.height / 20); // Approximate line height
-
+
return { cols: Math.max(80, cols), rows: Math.max(24, rows) };
}
@@ -14149,7 +15558,7 @@ class ClaudeOrchestrator {
if (startupUI) {
startupUI.style.display = 'none';
}
-
+
} catch (error) {
console.error('Error auto-starting Claude:', error);
this.showError('Failed to start Claude with settings');
@@ -14178,7 +15587,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
hideClaudeStartupModal() {
const modal = document.getElementById('claude-startup-modal');
if (modal) {
@@ -14186,7 +15595,7 @@ class ClaudeOrchestrator {
this.pendingClaudeSession = null;
}
}
-
+
async startClaudeWithOptions(sessionId, mode, skipPermissions) {
if (!this.socket || !this.socket.connected) {
this.showError('Not connected to server');
@@ -14316,7 +15725,7 @@ class ClaudeOrchestrator {
if (startupUI) startupUI.style.display = 'none';
this.dismissedStartupUI.set(sid, true);
}
-
+
quickStartClaude(sessionId, mode) {
// Check if YOLO mode is enabled
const yoloCheckbox = document.getElementById(`yolo-${sessionId}`);
@@ -14817,7 +16226,7 @@ class ClaudeOrchestrator {
document.addEventListener('keydown', handleEsc);
});
}
-
+
updateYoloState(sessionId, checked) {
// Update button styles to show YOLO is active
const buttons = [
@@ -14825,7 +16234,7 @@ class ClaudeOrchestrator {
document.getElementById(`btn-continue-${sessionId}`),
document.getElementById(`btn-resume-${sessionId}`)
];
-
+
buttons.forEach(btn => {
if (btn) {
if (checked) {
@@ -14836,25 +16245,25 @@ class ClaudeOrchestrator {
}
});
}
-
+
async startClaudeFromTerminal(sessionId) {
if (!this.socket || !this.socket.connected) {
return;
}
-
+
try {
// Get effective settings for this session
const response = await fetch(`/api/user-settings/effective/${sessionId}`);
let effectiveSettings = { claudeFlags: { skipPermissions: false } };
-
+
if (response.ok) {
effectiveSettings = await response.json();
}
-
+
// Get selected options from the inline UI, but use effective settings as fallback
const mode = document.querySelector(`input[name="claude-mode-${sessionId}"]:checked`)?.value || 'fresh';
const skipPermissions = document.getElementById(`skip-permissions-${sessionId}`)?.checked ?? effectiveSettings.claudeFlags.skipPermissions;
-
+
// Send command to server
this.socket.emit('start-claude', {
sessionId: sessionId,
@@ -14863,19 +16272,19 @@ class ClaudeOrchestrator {
skipPermissions: skipPermissions
}
});
-
+
// Hide the startup UI
const startupUI = document.getElementById(this.getSessionDomId('startup-ui', sessionId));
if (startupUI) {
startupUI.style.display = 'none';
}
-
+
// Enable the start button for future use
const startBtn = document.getElementById(`claude-start-btn-${sessionId}`);
if (startBtn) {
startBtn.disabled = false;
}
-
+
} catch (error) {
console.error('Error starting Claude from terminal:', error);
}
@@ -14883,10 +16292,10 @@ class ClaudeOrchestrator {
restartClaudeSession(sessionId) {
console.log(`Restarting Claude session: ${sessionId}`);
-
+
if (this.socket && this.socket.connected) {
this.socket.emit('restart-session', { sessionId });
-
+
// Update UI to show restarting
this.updateSessionStatus(sessionId, 'restarting');
} else {
@@ -14922,16 +16331,16 @@ class ClaudeOrchestrator {
if (!this.userSettings) {
console.warn('User settings not loaded, attempting to load...');
await this.loadUserSettings();
-
+
if (!this.userSettings) {
console.error('Failed to load user settings');
return;
}
}
-
+
const pathParts = path.split('.');
const newGlobal = JSON.parse(JSON.stringify(this.userSettings.global));
-
+
// Navigate to the correct nested property
let current = newGlobal;
for (let i = 0; i < pathParts.length - 1; i++) {
@@ -15556,7 +16965,7 @@ class ClaudeOrchestrator {
this.userSettings = updatedSettings;
this.syncUserSettingsUI();
console.log('Reset to defaults successfully');
-
+
// Show user feedback
this.showTemporaryMessage('Settings reset to defaults');
} else {
@@ -15582,7 +16991,7 @@ class ClaudeOrchestrator {
if (response.ok) {
console.log('Saved as default template successfully');
-
+
// Show user feedback with commit reminder
this.showTemporaryMessage('Settings saved as default template. Remember to commit and push the changes to user-settings.default.json!', 'success');
} else {
@@ -15600,7 +17009,7 @@ class ClaudeOrchestrator {
const messageEl = document.createElement('div');
messageEl.className = `temporary-message ${type}`;
messageEl.textContent = message;
-
+
// Style the message
messageEl.style.cssText = `
position: fixed;
@@ -15616,14 +17025,14 @@ class ClaudeOrchestrator {
transform: translateX(100%);
transition: transform 0.3s ease;
`;
-
+
document.body.appendChild(messageEl);
-
+
// Animate in
setTimeout(() => {
messageEl.style.transform = 'translateX(0)';
}, 100);
-
+
// Remove after delay
setTimeout(() => {
messageEl.style.transform = 'translateX(100%)';
@@ -15642,9 +17051,9 @@ class ClaudeOrchestrator {
this.showTemporaryMessage('Invalid session ID for replay viewer', 'error');
return;
}
-
+
const worktreeNum = worktreeMatch[1];
-
+
// Get worktree configuration from server for accurate path
let worktreeConfig = null;
try {
@@ -15655,19 +17064,19 @@ class ClaudeOrchestrator {
} catch (error) {
console.warn('Could not get worktree config, using defaults:', error);
}
-
+
// Use server-hosted replay viewer (avoids browser file:// restrictions)
const replayViewerUrl = `${window.location.origin}/replay-viewer/work${worktreeNum}/`;
-
+
console.log(`Opening replay viewer for ${sessionId} at ${replayViewerUrl}`);
-
+
// Open in new tab (simpler approach)
window.open(replayViewerUrl, '_blank');
-
+
// Show success message with URL for reference
this.showTemporaryMessage(`Opening replay viewer for work${worktreeNum}`, 'success');
console.log(`Replay viewer URL: ${replayViewerUrl}`);
-
+
} catch (error) {
console.error('Error opening replay viewer:', error);
this.showTemporaryMessage('Failed to open replay viewer', 'error');
@@ -15687,7 +17096,7 @@ class ClaudeOrchestrator {
setTimeout(checkAndStart, 500); // Check again in 500ms
}
};
-
+
setTimeout(checkAndStart, 1000); // Initial delay for terminal setup
}
@@ -15696,7 +17105,7 @@ class ClaudeOrchestrator {
const response = await fetch('/api/user-settings/check-updates');
if (response.ok) {
const result = await response.json();
-
+
if (result && result.hasUpdates) {
const notification = document.getElementById('settings-update-notification');
notification.classList.remove('hidden');
@@ -15802,17 +17211,17 @@ class ClaudeOrchestrator {
try {
this.showTemporaryMessage('Checking for updates...', 'info');
-
+
const response = await fetch('/api/git/check-updates');
if (response.ok) {
const result = await response.json();
-
+
if (result.hasUpdates) {
const notification = document.getElementById('git-update-notification');
const textElement = document.getElementById('git-notification-text');
textElement.textContent = `${result.commitsBehind} update${result.commitsBehind > 1 ? 's' : ''} available on ${result.currentBranch}`;
notification.classList.remove('hidden');
-
+
this.showTemporaryMessage(`Found ${result.commitsBehind} update${result.commitsBehind > 1 ? 's' : ''} available`, 'success');
} else if (result.hasUpdates === false) {
this.showTemporaryMessage('Repository is up to date', 'success');
@@ -15835,7 +17244,7 @@ class ClaudeOrchestrator {
}
this.showTemporaryMessage('Pulling latest changes...', 'info');
-
+
const response = await fetch('/api/git/pull', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
@@ -15843,14 +17252,14 @@ class ClaudeOrchestrator {
if (response.ok) {
const result = await response.json();
-
+
if (result.success) {
// Success message will be handled by socket event
const notification = document.getElementById('git-update-notification');
notification.classList.add('hidden');
} else {
this.showTemporaryMessage(result.error || 'Failed to pull changes', 'error');
-
+
// Show specific error details if available
if (result.changes && result.changes.length > 0) {
console.log('Uncommitted changes:', result.changes);
@@ -19679,7 +21088,7 @@ class ClaudeOrchestrator {
applyView();
return;
}
-
+
state.selectedCardId = card.id || null;
applyView();
@@ -21029,7 +22438,7 @@ class ClaudeOrchestrator {
col.style.setProperty('--tasks-card-rows', '1');
return;
}
-
+
const containerHeight = cardsContainer.clientHeight;
if (!containerHeight || containerHeight < 40) {
col.style.setProperty('--tasks-card-columns', '1');
@@ -21047,7 +22456,7 @@ class ClaudeOrchestrator {
return;
}
delete col.dataset.tasksWrapExpandRetry;
-
+
const styles = window.getComputedStyle(cardsContainer);
const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0;
const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0;
@@ -21092,10 +22501,10 @@ class ClaudeOrchestrator {
col.style.minWidth = `${Math.round(target)}px`;
}
};
-
+
apply(rowsFit);
const fits = () => (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1);
-
+
// If we still overflow vertically, reduce rows (creating more columns) until we fit.
for (let attempt = 0; attempt < 24; attempt++) {
// Force reflow and then check overflow.
@@ -30324,7 +31733,7 @@ class ClaudeOrchestrator {
// While worktree sessions are spinning up, we reserve the worktree so it isn't recommended again.
this.cleanupExpiredWorktreeReservations();
if (this.isWorktreeReserved(repoPathNorm, worktreeId)) return true;
-
+
// Extract repo name from path for session matching
const repoName = (repoNameOverride || repoPathNorm.split('/').pop() || '').toLowerCase();
diff --git a/client/index.html b/client/index.html
index 0605696e..e3c58b56 100644
--- a/client/index.html
+++ b/client/index.html
@@ -1,5 +1,6 @@
+
@@ -7,16 +8,18 @@
-
+
-
+
-
+
+
-
+
@@ -131,7 +134,7 @@
Claude Orchestrator
-
+
@@ -148,7 +151,7 @@
View Presets
-
+
@@ -191,7 +194,7 @@
Start AI Agent -
-
+
@@ -302,7 +306,8 @@
Workflow Notifications
-
Quiet: log only. Normal: toast for key events. Aggressive: toasts + optional browser/sound alerts.
+
Quiet: log only. Normal: toast for key events. Aggressive: toasts +
+ optional browser/sound alerts.
-
Controls the initial layout preset when opening the Review Console.
+
Controls the initial layout preset when opening the Review Console.
+
-
When enabled, Review Console includes the paired server tile next to the agent tile (when available).
+
When enabled, Review Console includes the paired server tile next to
+ the agent tile (when available).
@@ -546,22 +553,28 @@
Glossary
Workspace
- A saved configuration that groups worktrees/terminals for a project (or many repos in a mixed workspace). Opening a workspace starts its sessions.
+ A saved configuration that groups worktrees/terminals for a project
+ (or many repos in a mixed workspace). Opening a workspace starts its sessions.
Worktree
- A git worktree folder (e.g. work6) on disk. One worktree usually has an Agent terminal and (optionally) a paired Server terminal.
+ A git worktree folder (e.g. work6) on disk. One
+ worktree usually has an Agent terminal and (optionally) a paired Server
+ terminal.
Terminal / Session
- A running process (PTY) owned by the Orchestrator. It can be recovered after restart unless you explicitly close it.
+ A running process (PTY) owned by the Orchestrator. It can be
+ recovered after restart unless you explicitly close it.
Agent vs Server
- Agent runs Claude/Codex work. Server runs your dev server (game/app). Theyโre paired per worktree and should generally live/die together.
+ Agent runs Claude/Codex work.
+ Server runs your dev server (game/app). Theyโre paired per worktree and should
+ generally live/die together.
@@ -785,7 +798,8 @@ PR Merge Automation
Enable PR-merge automation (Trello)
- When a PR merges, auto-move/comment on the linked Trello card (when a Trello card is linked to the PR task).
+ When a PR merges, auto-move/comment on the linked Trello card (when a
+ Trello card is linked to the PR task).
-
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
-
+
@@ -866,7 +884,7 @@ Terminal Settings
Apply --dangerously-skip-permissions to all new Claude sessions
-
+
@@ -894,7 +912,8 @@ Terminal Settings
Start Delay (ms):
-
+
@@ -939,10 +958,11 @@
Terminal Settings
Per-Terminal Overrides
-
Override global settings for specific terminals (requires terminal restart)
+
Override global settings for specific terminals (requires terminal
+ restart)
-
+
Default Template Management
Manage the default settings template committed to the repository
@@ -962,10 +982,11 @@
Default Template Management
-
+
Repository Updates
-
Web/dev mode: Git pull updates. Tauri desktop mode: app updater check/install.
+
Web/dev mode: Git pull updates. Tauri desktop mode: app updater
+ check/install.
+
+
+
-
+
-
+
@@ -1035,6 +1075,7 @@ Notifications
+
@@ -1044,10 +1085,12 @@
Notifications
-
+
-
+
@@ -1062,4 +1105,5 @@
Notifications
+
diff --git a/client/notifications.js b/client/notifications.js
index 6fa218c3..7ec3ea9d 100644
--- a/client/notifications.js
+++ b/client/notifications.js
@@ -389,21 +389,21 @@ class NotificationManager {
// Add notification styles
const notificationStyles = document.createElement('style');
notificationStyles.textContent = `
- .empty-message {
+ .notifications-panel .empty-message {
padding: var(--space-xl);
text-align: center;
color: var(--text-secondary);
font-size: 0.875rem;
}
- .notification-header {
+ .notifications-panel .notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xs);
}
- .notification-meta {
+ .notifications-panel .notification-meta {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: var(--space-xs);
diff --git a/client/styles.css b/client/styles.css
index eab6a28d..62b1bf22 100644
--- a/client/styles.css
+++ b/client/styles.css
@@ -79,6 +79,15 @@
--shadow-float: 0 18px 40px rgba(0, 0, 0, 0.45);
--shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.15);
--focus-terminal-bg: color-mix(in srgb, var(--bg-primary) 72%, #000000 28%);
+
+ /* Scrollbars */
+ --scrollbar-size: 10px;
+ --scrollbar-radius: 999px;
+ --scrollbar-track: #1b2230;
+ --scrollbar-thumb: #3f5f8f;
+ --scrollbar-thumb-hover: #4f76ae;
+ --scrollbar-thumb-active: #5a84bf;
+ --scrollbar-thumb-border: rgba(13, 17, 23, 0.72);
}
/* Light Theme */
@@ -107,6 +116,30 @@ body.light-theme {
--shadow-float: 0 18px 40px rgba(0, 0, 0, 0.22);
--shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.12);
--focus-terminal-bg: color-mix(in srgb, var(--bg-primary) 94%, #000000 6%);
+
+ --scrollbar-track: #dbe1e8;
+ --scrollbar-thumb: #7f91aa;
+ --scrollbar-thumb-hover: #6f86a8;
+ --scrollbar-thumb-active: #5d789d;
+ --scrollbar-thumb-border: rgba(255, 255, 255, 0.76);
+}
+
+@supports (color: color-mix(in srgb, #000000 50%, #ffffff 50%)) {
+ :root {
+ --scrollbar-track: color-mix(in srgb, var(--bg-secondary) 78%, #000000 22%);
+ --scrollbar-thumb: color-mix(in srgb, var(--accent-primary) 46%, var(--bg-tertiary) 54%);
+ --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-primary-hover) 60%, var(--bg-tertiary) 40%);
+ --scrollbar-thumb-active: color-mix(in srgb, var(--accent-primary) 72%, var(--bg-tertiary) 28%);
+ --scrollbar-thumb-border: color-mix(in srgb, var(--bg-primary) 76%, transparent 24%);
+ }
+
+ body.light-theme {
+ --scrollbar-track: color-mix(in srgb, var(--bg-tertiary) 70%, #ffffff 30%);
+ --scrollbar-thumb: color-mix(in srgb, var(--accent-primary) 44%, #7f8a98 56%);
+ --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-primary) 58%, #6f7c8f 42%);
+ --scrollbar-thumb-active: color-mix(in srgb, var(--accent-primary) 68%, #617187 32%);
+ --scrollbar-thumb-border: color-mix(in srgb, #ffffff 68%, transparent 32%);
+ }
}
/* Skins */
@@ -178,6 +211,133 @@ body.light-theme.skin-high-contrast {
margin: 0;
padding: 0;
box-sizing: border-box;
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+}
+
+*::-webkit-scrollbar {
+ width: var(--scrollbar-size);
+ height: var(--scrollbar-size);
+}
+
+*::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
+}
+
+*::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-thumb-hover);
+}
+
+*::-webkit-scrollbar-thumb:active {
+ background: var(--scrollbar-thumb-active);
+}
+
+*::-webkit-scrollbar-corner {
+ background: var(--scrollbar-track);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+) {
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar {
+ width: var(--scrollbar-size);
+ height: var(--scrollbar-size);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-thumb-hover);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-thumb:active {
+ background: var(--scrollbar-thumb-active);
}
body {
@@ -2143,12 +2303,19 @@ header h1 {
}
.worktree-inspector-header.review-console-header::-webkit-scrollbar {
- height: 6px;
+ height: 8px;
+}
+
+.worktree-inspector-header.review-console-header::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
}
.worktree-inspector-header.review-console-header::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.16);
- border-radius: 999px;
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
}
.review-console-route-bar {
@@ -4354,19 +4521,23 @@ header h1 {
line-height: 1.35;
}
-.settings-toolbar input {
- flex: 1 1 auto;
- min-width: 0;
- padding: 0 10px;
+body.dependency-onboarding-active {
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 12% 18%, rgba(56, 139, 253, 0.16), transparent 48%),
+ radial-gradient(circle at 88% 8%, rgba(34, 197, 94, 0.14), transparent 44%),
+ var(--bg-primary);
}
-.settings-toolbar select {
- flex: 0 0 auto;
- min-width: 120px;
- padding: 0 8px;
+body.dependency-onboarding-booting {
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 14% 22%, rgba(56, 139, 253, 0.14), transparent 46%),
+ radial-gradient(circle at 82% 10%, rgba(34, 197, 94, 0.12), transparent 42%),
+ var(--bg-primary);
}
-.settings-filter-hidden {
+body.dependency-onboarding-booting > :not(#dependency-setup-modal):not(#toast-stack):not(.toast):not(.ready-toast):not(#notifications-panel):not(.cross-workspace-notifications-area) {
display: none !important;
}
@@ -4416,57 +4587,45 @@ header h1 {
border-radius: 10px;
}
-.setting-group {
- margin-bottom: var(--space-md);
+body.dependency-onboarding-active > :not(#dependency-setup-modal):not(#toast-stack):not(.toast):not(.ready-toast):not(#notifications-panel):not(.cross-workspace-notifications-area) {
+ display: none !important;
}
-.setting-group label {
+body.dependency-onboarding-active #dependency-setup-modal {
display: flex;
align-items: center;
- gap: var(--space-sm);
- cursor: pointer;
-}
-
-.setting-group select {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- padding: var(--space-xs) var(--space-sm);
- border-radius: var(--radius-sm);
- margin-left: var(--space-sm);
+ justify-content: center;
+ padding: 24px;
+ z-index: 4500;
}
-.setting-section {
- margin: var(--space-lg) 0;
- padding-top: var(--space-lg);
- border-top: 1px solid var(--border-color);
+.dependency-setup-content {
+ max-width: min(760px, 94vw);
+ width: min(760px, 94vw);
+ border: 1px solid rgba(56, 139, 253, 0.2);
+ box-shadow: 0 24px 54px rgba(0, 0, 0, 0.35);
}
-.setting-section h4 {
- color: var(--text-primary);
- margin-bottom: var(--space-md);
- font-size: 1.1em;
+.dependency-setup-content .modal-header {
+ margin-bottom: 10px;
}
-.setting-section h5 {
- color: var(--text-secondary);
- margin-bottom: var(--space-sm);
- font-size: 0.9em;
- font-weight: 600;
+.dependency-setup-content .modal-header h3 {
+ font-size: 1.2rem;
+ letter-spacing: 0.01em;
}
-.setting-group label small {
- display: block;
- color: var(--text-tertiary);
- font-size: 0.8em;
- margin-top: var(--space-xs);
- margin-left: var(--space-lg);
+.dependency-setup-body {
+ max-height: 68vh;
+ overflow: auto;
+ padding-right: 4px;
}
-.setting-description {
- color: var(--text-tertiary);
- font-size: 0.85em;
- margin-bottom: var(--space-md);
+.dependency-setup-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ margin-top: 12px;
}
.skin-gallery {
@@ -4539,612 +4698,1216 @@ header h1 {
.settings-glossary details {
border: 1px solid var(--border-color);
- border-radius: var(--radius-sm);
- padding: 8px 10px;
+ border-radius: var(--radius-lg);
background: var(--bg-tertiary);
- margin: 10px 0;
-}
-
-.settings-glossary details[open] {
- background: rgba(255, 255, 255, 0.02);
+ padding: 14px;
}
-.settings-glossary summary {
- cursor: pointer;
+.dependency-setup-item-header {
display: flex;
align-items: center;
- gap: 8px;
- list-style: none;
-}
-
-.settings-glossary summary::-webkit-details-marker {
- display: none;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 6px;
+ flex-wrap: wrap;
}
-.settings-glossary details > .setting-description {
- margin: 10px 0 0;
- opacity: 0.95;
+.dependency-setup-item-title {
+ font-weight: 650;
+ font-size: 1.03rem;
+ color: var(--text-primary);
}
-.identity-saved-list {
+.dependency-setup-badges {
display: flex;
- flex-wrap: wrap;
+ align-items: center;
gap: 6px;
+ flex-wrap: wrap;
}
-.identity-chip {
+.dependency-setup-badge {
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 2px 8px;
- border-radius: var(--radius-sm);
+ border-radius: 999px;
border: 1px solid var(--border-color);
- background: rgba(255, 255, 255, 0.03);
- font-family: var(--font-mono);
- font-size: 0.78rem;
- max-width: 100%;
+ padding: 2px 8px;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ background: var(--bg-secondary);
}
-.identity-chip button {
- border: none;
- background: transparent;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 0;
- line-height: 1;
+.dependency-setup-badge.status-ok {
+ color: var(--accent-success);
+ border-color: rgba(34, 197, 94, 0.45);
}
-.identity-chip button:hover {
- color: var(--text-secondary);
+.dependency-setup-badge.status-missing {
+ color: var(--accent-warning);
+ border-color: rgba(245, 158, 11, 0.45);
}
-.per-terminal-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--space-sm);
- margin-bottom: var(--space-xs);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
+.dependency-setup-badge.status-pending {
+ color: var(--accent-primary);
+ border-color: rgba(56, 139, 253, 0.45);
}
-.per-terminal-item .terminal-name {
- color: var(--text-primary);
- font-weight: 500;
- font-family: var(--font-mono);
+.dependency-setup-badge.level-required {
+ color: var(--accent-danger);
+ border-color: rgba(239, 68, 68, 0.45);
}
-.per-terminal-item .terminal-override {
- color: var(--text-secondary);
- font-size: 0.8em;
+.dependency-setup-badge.level-recommended {
+ color: var(--accent-primary);
+ border-color: rgba(56, 139, 253, 0.45);
}
-.per-terminal-items {
- margin-top: var(--space-sm);
+.dependency-setup-badge.level-optional {
+ color: var(--accent-warning);
+ border-color: rgba(245, 158, 11, 0.45);
}
-/* Notifications Panel */
-.notifications-panel {
- position: fixed;
- top: var(--header-height);
- right: 0;
- bottom: 0;
- width: 380px;
+.dependency-setup-item-desc {
+ color: var(--text-secondary);
+ margin-bottom: 10px;
+ line-height: 1.45;
+}
+
+.dependency-setup-item-command {
+ margin: 0;
+ padding: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
background: var(--bg-secondary);
- border-left: 1px solid var(--border-color);
- box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15);
- transform: translateX(100%);
- transition: transform 0.3s;
- z-index: 101;
- display: flex;
- flex-direction: column;
+ color: var(--text-secondary);
+ white-space: pre-wrap;
+ font-size: 0.85rem;
}
-.notifications-panel:not(.hidden) {
- transform: translateX(0);
+.dependency-onboarding-command-wrap {
+ margin-bottom: 10px;
}
-.notifications-content {
- padding: var(--space-md);
- overflow-y: auto;
- flex: 1;
+.dependency-onboarding-command-label {
+ margin: 0 0 6px;
+ font-size: 0.76rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--text-tertiary);
}
-.notification-list {
+.dependency-setup-item-actions {
display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-.notification-item {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-tertiary);
- padding: var(--space-sm);
- cursor: pointer;
- transition: border-color 0.15s, background 0.15s;
+.dependency-setup-item-actions .btn-secondary {
+ width: auto;
+ min-height: 32px;
+ flex: 0 0 auto;
}
-.notification-item:hover {
- border-color: rgba(56, 139, 253, 0.55);
- background: rgba(56, 139, 253, 0.06);
+.dependency-setup-item-actions a.btn-secondary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: none;
}
-.notification-item.unread {
- border-left: 4px solid var(--accent-primary);
+.dependency-setup-actions {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ gap: 8px;
}
-.notification-type {
- font-size: 0.75rem;
- padding: 2px 6px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
+.dependency-setup-actions .btn-secondary {
+ flex: 0 0 auto;
+}
+
+.dependency-setup-empty {
+ padding: 12px;
+ border: 1px dashed var(--border-color);
+ border-radius: var(--radius-sm);
color: var(--text-secondary);
}
-.notification-type.waiting {
- border-color: rgba(245, 158, 11, 0.45);
- color: var(--accent-warning);
+.dependency-onboarding-progress {
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary);
+ padding: 12px;
}
-.notification-type.completed {
- border-color: rgba(34, 197, 94, 0.45);
- color: var(--accent-success);
+.dependency-onboarding-progress-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+ color: var(--text-secondary);
}
-.notification-type.error {
- border-color: rgba(239, 68, 68, 0.45);
- color: var(--accent-danger);
+.dependency-onboarding-progress-track {
+ width: 100%;
+ height: 8px;
+ border-radius: 999px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ overflow: hidden;
}
-.notification-message {
- color: var(--text-primary);
- line-height: 1.35;
+.dependency-onboarding-progress-bar {
+ height: 100%;
+ background: var(--accent-primary);
+ transition: width 180ms ease-in-out;
}
-.notification-actions {
- display: flex;
- gap: var(--space-xs);
- margin-top: var(--space-sm);
- justify-content: flex-end;
+.dependency-onboarding-step {
+ margin-top: 12px;
}
-.notification-action {
- padding: 4px 8px;
- font-size: 0.8rem;
- width: auto;
+.dependency-onboarding-step-card {
+ background:
+ linear-gradient(150deg, rgba(56, 139, 253, 0.09), rgba(34, 197, 94, 0.03)),
+ var(--bg-tertiary);
}
-.terminal-controls {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
+.dependency-onboarding-step-kicker {
+ margin-bottom: 6px;
+ font-size: 0.74rem;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--text-tertiary);
}
-.terminal-controls label {
- font-size: 0.9em;
- margin: 0;
+.dependency-onboarding-state {
+ margin: 0 0 8px;
+ color: var(--text-secondary);
+ font-weight: 500;
}
-/* Loading spinner for build button */
-@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
+.dependency-onboarding-state.status-ok {
+ color: var(--accent-success);
}
-/* Server Launch Settings */
-.server-launch-group {
- display: inline-flex;
- gap: 4px;
- align-items: center;
+.dependency-onboarding-state.status-missing {
+ color: var(--accent-warning);
}
-#launch-settings-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: var(--overlay-backdrop);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 2000;
+.dependency-onboarding-state.status-pending {
+ color: var(--accent-primary);
}
-#launch-settings-modal .modal-content {
- background: var(--bg-primary);
- border-radius: var(--radius-lg);
- max-width: 800px;
- width: 90%;
- max-height: 90vh;
- overflow-y: auto;
- box-shadow: var(--shadow-modal);
+.dependency-gh-login-helper {
+ margin: 0 0 10px;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background:
+ linear-gradient(145deg, rgba(56, 139, 253, 0.1), rgba(56, 139, 253, 0.02)),
+ var(--bg-tertiary);
}
-#launch-settings-modal .modal-header {
- padding: var(--space-lg);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
+.dependency-git-identity-helper {
+ margin: 0 0 10px;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background:
+ linear-gradient(145deg, rgba(34, 197, 94, 0.08), rgba(34, 197, 94, 0.02)),
+ var(--bg-tertiary);
}
-#launch-settings-modal .modal-header h2 {
- margin: 0;
- font-size: 1.5rem;
- color: var(--text-primary);
+.dependency-git-identity-fields {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
+ gap: 8px;
+ margin-bottom: 8px;
}
-#launch-settings-modal .close-btn {
- background: transparent;
- border: none;
- color: var(--text-secondary);
- font-size: 1.5rem;
- cursor: pointer;
- padding: 0;
- width: 32px;
- height: 32px;
+.dependency-git-identity-field {
display: flex;
- align-items: center;
- justify-content: center;
- border-radius: var(--radius-sm);
+ flex-direction: column;
+ gap: 4px;
+ color: var(--text-secondary);
+ font-size: 0.86rem;
}
-#launch-settings-modal .close-btn:hover {
+.dependency-git-identity-field input {
+ width: 100%;
+ min-height: 34px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
background: var(--bg-secondary);
+ color: var(--text-primary);
+ padding: 0 10px;
+ font-size: 0.9rem;
}
-#launch-settings-modal .modal-body {
- padding: var(--space-lg);
-}
-
-#launch-settings-modal .settings-section {
- margin-bottom: var(--space-xl);
-}
-
-#launch-settings-modal .settings-section:last-child {
- margin-bottom: 0;
-}
-
-#launch-settings-modal .settings-section h3 {
- color: var(--accent-primary);
- font-size: 1.1rem;
- margin-bottom: var(--space-md);
+.dependency-git-identity-field input:focus {
+ outline: none;
+ border-color: var(--accent-primary);
}
-#launch-settings-modal .setting-group {
- margin-bottom: var(--space-md);
+.dependency-git-identity-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-#launch-settings-modal .setting-group label {
- display: block;
- color: var(--text-secondary);
- font-size: 0.9rem;
- margin-bottom: var(--space-xs);
+.dependency-git-help-btn {
+ min-width: 32px;
+ width: 32px;
+ padding: 0;
+ font-weight: 700;
}
-#launch-settings-modal .setting-group input {
- width: 100%;
- padding: var(--space-sm) var(--space-md);
- background: var(--bg-secondary);
+.dependency-git-help-inline {
+ margin-top: 8px;
+ padding: 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
- color: var(--text-primary);
- font-family: monospace;
+ background: var(--bg-secondary);
}
-#launch-settings-modal .setting-group input:focus {
- outline: none;
- border-color: var(--accent-primary);
+.dependency-git-help-line {
+ color: var(--text-secondary);
+ line-height: 1.45;
}
-#launch-settings-modal .setting-group small {
- display: block;
- color: var(--text-tertiary);
- font-size: 0.8rem;
- margin-top: var(--space-xs);
+.dependency-git-help-line + .dependency-git-help-line {
+ margin-top: 6px;
}
-#launch-settings-modal .preset-checkboxes {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: var(--space-md);
+.dependency-gh-login-helper-text {
+ color: var(--text-secondary);
+ margin-bottom: 8px;
}
-#launch-settings-modal .preset-checkbox {
+.dependency-gh-login-code-wrap {
display: flex;
align-items: center;
- padding: var(--space-md);
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- user-select: none;
-}
-
-#launch-settings-modal .preset-checkbox:hover {
- background: var(--bg-tertiary);
- border-color: var(--accent-primary);
+ gap: 8px;
+ flex-wrap: wrap;
}
-#launch-settings-modal .preset-checkbox input[type="checkbox"] {
- margin-right: var(--space-sm);
- width: 18px;
- height: 18px;
- cursor: pointer;
+.dependency-gh-login-helper-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-top: 8px;
}
-#launch-settings-modal .preset-checkbox input[type="checkbox"]:checked + span {
- color: var(--accent-primary);
+.dependency-gh-login-code {
+ display: inline-block;
+ padding: 7px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ letter-spacing: 0.06em;
font-weight: 600;
}
-#launch-settings-modal .preset-checkbox span {
- font-size: 0.9rem;
- transition: all 0.2s;
+.dependency-setup-item-output {
+ max-height: 150px;
+ overflow: auto;
}
-#launch-settings-modal .modal-footer {
- padding: var(--space-lg);
- border-top: 1px solid var(--border-color);
+.dependency-onboarding-nav {
display: flex;
+ align-items: center;
justify-content: flex-end;
- gap: var(--space-md);
+ gap: 8px;
+ margin-top: 12px;
}
-#launch-settings-modal .btn-save,
-#launch-settings-modal .btn-cancel {
- padding: var(--space-sm) var(--space-lg);
- border-radius: var(--radius-sm);
- cursor: pointer;
- font-size: 0.9rem;
- transition: all 0.2s;
+.dependency-onboarding-nav .btn-secondary,
+.dependency-onboarding-nav .btn-primary {
+ width: auto;
+ min-height: 34px;
+ flex: 0 0 auto;
}
-#launch-settings-modal .btn-save {
- background: var(--accent-primary);
- color: white;
- border: none;
-}
+@media (max-width: 700px) {
+ body.dependency-onboarding-active #dependency-setup-modal {
+ padding: 14px;
+ }
-#launch-settings-modal .btn-save:hover {
- background: var(--accent-secondary);
-}
+ .dependency-setup-content {
+ width: min(760px, 96vw);
+ }
-#launch-settings-modal .btn-cancel {
- background: var(--bg-secondary);
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
+ .dependency-setup-actions {
+ justify-content: flex-start;
+ }
+
+ .dependency-onboarding-nav {
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ }
}
-#launch-settings-modal .btn-cancel:hover {
- background: var(--bg-tertiary);
+.settings-toolbar input {
+ flex: 1 1 auto;
+ min-width: 0;
+ padding: 0 10px;
}
-.loading-spinner {
- display: inline-block;
- width: 14px;
- height: 14px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-top: 2px solid #fff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
+.settings-toolbar select {
+ flex: 0 0 auto;
+ min-width: 120px;
+ padding: 0 8px;
}
-.control-btn.building {
- background: var(--color-warning);
- cursor: not-allowed;
- opacity: 0.8;
+.settings-filter-hidden {
+ display: none !important;
}
-.clear-override-btn {
- background: var(--bg-primary);
- color: var(--text-secondary);
+/* Review Console */
+.review-console-warning {
+ margin: 10px 12px 14px;
+ padding: 10px 12px;
border: 1px solid var(--border-color);
- border-radius: var(--radius-sm);
- padding: var(--space-xs);
- cursor: pointer;
- font-size: 0.8em;
- transition: background-color 0.2s;
+ border-left: 4px solid var(--accent-warning);
+ background: rgba(210, 153, 34, 0.10);
+ border-radius: 10px;
}
-.clear-override-btn:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
+.setting-group {
+ margin-bottom: var(--space-md);
}
-.template-actions {
+.setting-group label {
display: flex;
+ align-items: center;
gap: var(--space-sm);
- margin-top: var(--space-sm);
+ cursor: pointer;
}
-.template-btn {
- padding: var(--space-sm) var(--space-md);
+.setting-group select {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
border: 1px solid var(--border-color);
+ padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
- cursor: pointer;
- font-size: 0.9em;
- transition: all 0.2s;
+ margin-left: var(--space-sm);
}
-.template-btn.primary {
- background: var(--accent-primary);
- color: white;
- border-color: var(--accent-primary);
+.setting-section {
+ margin: var(--space-lg) 0;
+ padding-top: var(--space-lg);
+ border-top: 1px solid var(--border-color);
}
-.template-btn.primary:hover {
- background: var(--accent-primary-hover);
- border-color: var(--accent-primary-hover);
+.setting-section h4 {
+ color: var(--text-primary);
+ margin-bottom: var(--space-md);
+ font-size: 1.1em;
}
-.template-btn.secondary {
- background: var(--bg-primary);
+.setting-section h5 {
color: var(--text-secondary);
- border-color: var(--border-color);
+ margin-bottom: var(--space-sm);
+ font-size: 0.9em;
+ font-weight: 600;
}
-.template-btn.secondary:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
+.setting-group label small {
+ display: block;
+ color: var(--text-tertiary);
+ font-size: 0.8em;
+ margin-top: var(--space-xs);
+ margin-left: var(--space-lg);
}
-.update-notification {
- margin-top: var(--space-sm);
- padding: var(--space-sm);
- background: var(--bg-tertiary);
- border: 1px solid var(--accent-warning);
+.setting-description {
+ color: var(--text-tertiary);
+ font-size: 0.85em;
+ margin-bottom: var(--space-md);
+}
+
+.settings-glossary details {
+ border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
- transition: all 0.3s;
+ padding: 8px 10px;
+ background: var(--bg-tertiary);
+ margin: 10px 0;
}
-.update-notification.hidden {
- display: none;
+.settings-glossary details[open] {
+ background: rgba(255, 255, 255, 0.02);
}
-.notification-content {
+.settings-glossary summary {
+ cursor: pointer;
display: flex;
align-items: center;
- gap: var(--space-sm);
+ gap: 8px;
+ list-style: none;
}
-.notification-icon {
- font-size: 1.1em;
+.settings-glossary summary::-webkit-details-marker {
+ display: none;
}
-.notification-text {
- flex: 1;
- color: var(--text-primary);
- font-size: 0.9em;
+.settings-glossary details > .setting-description {
+ margin: 10px 0 0;
+ opacity: 0.95;
}
-.dismiss-btn {
- background: none;
+.identity-saved-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.identity-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border-color);
+ background: rgba(255, 255, 255, 0.03);
+ font-family: var(--font-mono);
+ font-size: 0.78rem;
+ max-width: 100%;
+}
+
+.identity-chip button {
border: none;
- color: var(--text-secondary);
+ background: transparent;
+ color: var(--text-tertiary);
cursor: pointer;
- font-size: 1.2em;
padding: 0;
- width: 24px;
- height: 24px;
+ line-height: 1;
+}
+
+.identity-chip button:hover {
+ color: var(--text-secondary);
+}
+
+.per-terminal-item {
display: flex;
align-items: center;
- justify-content: center;
+ justify-content: space-between;
+ padding: var(--space-sm);
+ margin-bottom: var(--space-xs);
+ background: var(--bg-tertiary);
border-radius: var(--radius-sm);
- transition: all 0.2s;
}
-.dismiss-btn:hover {
- background: var(--bg-primary);
+.per-terminal-item .terminal-name {
color: var(--text-primary);
+ font-weight: 500;
+ font-family: var(--font-mono);
}
-/* Loading */
-.loading-message {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
+.per-terminal-item .terminal-override {
+ color: var(--text-secondary);
+ font-size: 0.8em;
}
-.spinner {
- width: 40px;
- height: 40px;
- border: 3px solid var(--bg-tertiary);
- border-top-color: var(--accent-primary);
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto var(--space-md);
+.per-terminal-items {
+ margin-top: var(--space-sm);
}
-@keyframes spin {
- to { transform: rotate(360deg); }
+/* Notifications Panel */
+.notifications-panel {
+ position: fixed;
+ top: var(--header-height);
+ right: 0;
+ bottom: 0;
+ width: min(420px, 96vw);
+ background:
+ linear-gradient(180deg, rgba(18, 24, 38, 0.94), rgba(10, 15, 27, 0.94));
+ border-left: 1px solid rgba(147, 197, 253, 0.22);
+ box-shadow: -20px 0 44px rgba(2, 6, 16, 0.58);
+ backdrop-filter: blur(12px);
+ transform: translateX(100%);
+ transition: transform 0.24s ease;
+ z-index: 12040;
+ display: flex;
+ flex-direction: column;
}
-@keyframes pulse {
- from { opacity: 0.5; }
- to { opacity: 1; }
+.notifications-panel:not(.hidden) {
+ transform: translateX(0);
}
-/* Toast Notifications */
-.ready-toast {
- position: fixed;
- top: calc(var(--header-height) + 20px);
- right: 20px;
- background: var(--accent-success);
- color: white;
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-soft);
- z-index: 1000;
- animation: slideInRight 0.3s ease-out, fadeOutRight 0.3s ease-in 2.7s forwards;
+.notifications-panel .panel-header {
+ padding: 14px 16px;
+ border-bottom: 1px solid rgba(147, 197, 253, 0.2);
+ background:
+ linear-gradient(180deg, rgba(35, 51, 74, 0.64), rgba(26, 38, 58, 0.38));
}
-.toast-content {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
+.notifications-panel .panel-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 640;
+ letter-spacing: 0.01em;
+ color: #f4f8ff;
}
-.toast-icon {
- font-size: 1.2rem;
+.notifications-panel .panel-actions .icon-button {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ border: 1px solid rgba(147, 197, 253, 0.24);
+ background: rgba(7, 12, 22, 0.56);
+ color: rgba(244, 248, 255, 0.9);
}
-.toast-text {
- font-weight: 500;
- font-size: 0.875rem;
+.notifications-panel .panel-actions .icon-button:hover {
+ border-color: rgba(147, 197, 253, 0.44);
+ background: rgba(29, 78, 216, 0.2);
}
-@keyframes slideInRight {
- from {
- transform: translateX(100%);
- opacity: 0;
- }
- to {
- transform: translateX(0);
- opacity: 1;
- }
+.notifications-content {
+ padding: 12px 14px 18px;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ flex: 1;
}
-@keyframes fadeOutRight {
- from {
- transform: translateX(0);
- opacity: 1;
- }
- to {
- transform: translateX(100%);
- opacity: 0;
- }
+.notification-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
}
-/* Responsive - ONLY apply if data-visible-count is not set */
-@media (max-width: 1200px) {
- .terminal-grid:not([data-visible-count]) {
- grid-template-columns: repeat(2, 1fr);
- }
+.notifications-panel .empty-message {
+ border: 1px dashed rgba(147, 197, 253, 0.24);
+ border-radius: 12px;
+ background: rgba(12, 19, 33, 0.62);
+ color: rgba(223, 231, 245, 0.76);
}
-@media (max-width: 768px) {
- .projects-chats-shell {
- grid-template-columns: 1fr;
- min-height: 70vh;
- }
+.notifications-panel .notification-item {
+ border: 1px solid rgba(147, 197, 253, 0.2);
+ border-radius: 14px;
+ background:
+ linear-gradient(180deg, rgba(17, 25, 40, 0.92), rgba(10, 16, 28, 0.92));
+ padding: 12px 12px 10px;
+ cursor: pointer;
+ transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
+ box-shadow: 0 8px 18px rgba(2, 6, 16, 0.34);
+}
- .sidebar-toggle {
- display: inline-flex;
- }
+.notifications-panel .notification-item:hover {
+ transform: translateY(-1px);
+ border-color: rgba(96, 165, 250, 0.5);
+ box-shadow: 0 12px 24px rgba(9, 30, 66, 0.42);
+}
- header {
- padding: 0 var(--space-md);
- }
+.notifications-panel .notification-item.unread {
+ border-left: 3px solid #60a5fa;
+ box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.22), 0 12px 24px rgba(9, 30, 66, 0.38);
+}
- .header-content {
- gap: var(--space-md);
- }
+.notifications-panel .notification-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 7px;
+ gap: 8px;
+}
+
+.notifications-panel .notification-time {
+ font-size: 0.75rem;
+ letter-spacing: 0.01em;
+ color: rgba(223, 231, 245, 0.66);
+}
+
+.notifications-panel .notification-type {
+ font-size: 0.69rem;
+ font-weight: 620;
+ letter-spacing: 0.055em;
+ text-transform: uppercase;
+ padding: 3px 8px;
+ border-radius: 999px;
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ background: rgba(15, 23, 42, 0.65);
+ color: rgba(226, 232, 240, 0.9);
+}
+
+.notifications-panel .notification-type.waiting {
+ border-color: rgba(245, 158, 11, 0.45);
+ background: rgba(245, 158, 11, 0.14);
+ color: #fcd34d;
+}
+
+.notifications-panel .notification-type.completed {
+ border-color: rgba(34, 197, 94, 0.45);
+ background: rgba(34, 197, 94, 0.14);
+ color: #86efac;
+}
+
+.notifications-panel .notification-type.error {
+ border-color: rgba(239, 68, 68, 0.45);
+ background: rgba(239, 68, 68, 0.12);
+ color: #fca5a5;
+}
+
+.notifications-panel .notification-message {
+ color: rgba(239, 245, 255, 0.94);
+ line-height: 1.42;
+ font-size: 0.89rem;
+ margin: 0;
+}
+
+.notifications-panel .notification-meta {
+ font-size: 0.75rem;
+ color: rgba(190, 206, 230, 0.72);
+ margin-top: 6px;
+}
+
+.notifications-panel .notification-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+
+.notifications-panel .notification-action {
+ min-width: 0;
+ width: auto;
+ border-radius: 8px;
+ border: 1px solid rgba(147, 197, 253, 0.24);
+ background: rgba(13, 20, 35, 0.6);
+ color: rgba(239, 245, 255, 0.9);
+ padding: 4px 10px;
+ font-size: 0.78rem;
+}
+
+.notifications-panel .notification-action:hover {
+ border-color: rgba(147, 197, 253, 0.45);
+ background: rgba(37, 99, 235, 0.24);
+}
+
+.terminal-controls {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.terminal-controls label {
+ font-size: 0.9em;
+ margin: 0;
+}
+
+/* Loading spinner for build button */
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Server Launch Settings */
+.server-launch-group {
+ display: inline-flex;
+ gap: 4px;
+ align-items: center;
+}
+
+#launch-settings-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--overlay-backdrop);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+}
+
+#launch-settings-modal .modal-content {
+ background: var(--bg-primary);
+ border-radius: var(--radius-lg);
+ max-width: 800px;
+ width: 90%;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: var(--shadow-modal);
+}
+
+#launch-settings-modal .modal-header {
+ padding: var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#launch-settings-modal .modal-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ color: var(--text-primary);
+}
+
+#launch-settings-modal .close-btn {
+ background: transparent;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 1.5rem;
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+}
+
+#launch-settings-modal .close-btn:hover {
+ background: var(--bg-secondary);
+}
+
+#launch-settings-modal .modal-body {
+ padding: var(--space-lg);
+}
+
+#launch-settings-modal .settings-section {
+ margin-bottom: var(--space-xl);
+}
+
+#launch-settings-modal .settings-section:last-child {
+ margin-bottom: 0;
+}
+
+#launch-settings-modal .settings-section h3 {
+ color: var(--accent-primary);
+ font-size: 1.1rem;
+ margin-bottom: var(--space-md);
+}
+
+#launch-settings-modal .setting-group {
+ margin-bottom: var(--space-md);
+}
+
+#launch-settings-modal .setting-group label {
+ display: block;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ margin-bottom: var(--space-xs);
+}
+
+#launch-settings-modal .setting-group input {
+ width: 100%;
+ padding: var(--space-sm) var(--space-md);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-family: monospace;
+}
+
+#launch-settings-modal .setting-group input:focus {
+ outline: none;
+ border-color: var(--accent-primary);
+}
+
+#launch-settings-modal .setting-group small {
+ display: block;
+ color: var(--text-tertiary);
+ font-size: 0.8rem;
+ margin-top: var(--space-xs);
+}
+
+#launch-settings-modal .preset-checkboxes {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-md);
+}
+
+#launch-settings-modal .preset-checkbox {
+ display: flex;
+ align-items: center;
+ padding: var(--space-md);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all 0.2s;
+ user-select: none;
+}
+
+#launch-settings-modal .preset-checkbox:hover {
+ background: var(--bg-tertiary);
+ border-color: var(--accent-primary);
+}
+
+#launch-settings-modal .preset-checkbox input[type="checkbox"] {
+ margin-right: var(--space-sm);
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+#launch-settings-modal .preset-checkbox input[type="checkbox"]:checked + span {
+ color: var(--accent-primary);
+ font-weight: 600;
+}
+
+#launch-settings-modal .preset-checkbox span {
+ font-size: 0.9rem;
+ transition: all 0.2s;
+}
+
+#launch-settings-modal .modal-footer {
+ padding: var(--space-lg);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-md);
+}
+
+#launch-settings-modal .btn-save,
+#launch-settings-modal .btn-cancel {
+ padding: var(--space-sm) var(--space-lg);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: all 0.2s;
+}
+
+#launch-settings-modal .btn-save {
+ background: var(--accent-primary);
+ color: white;
+ border: none;
+}
+
+#launch-settings-modal .btn-save:hover {
+ background: var(--accent-secondary);
+}
+
+#launch-settings-modal .btn-cancel {
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+}
+
+#launch-settings-modal .btn-cancel:hover {
+ background: var(--bg-tertiary);
+}
+
+.loading-spinner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top: 2px solid #fff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.control-btn.building {
+ background: var(--color-warning);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
+
+.clear-override-btn {
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ padding: var(--space-xs);
+ cursor: pointer;
+ font-size: 0.8em;
+ transition: background-color 0.2s;
+}
+
+.clear-override-btn:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.template-actions {
+ display: flex;
+ gap: var(--space-sm);
+ margin-top: var(--space-sm);
+}
+
+.template-btn {
+ padding: var(--space-sm) var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: 0.9em;
+ transition: all 0.2s;
+}
+
+.template-btn.primary {
+ background: var(--accent-primary);
+ color: white;
+ border-color: var(--accent-primary);
+}
+
+.template-btn.primary:hover {
+ background: var(--accent-primary-hover);
+ border-color: var(--accent-primary-hover);
+}
+
+.template-btn.secondary {
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ border-color: var(--border-color);
+}
+
+.template-btn.secondary:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.update-notification {
+ margin-top: var(--space-sm);
+ padding: var(--space-sm);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--accent-warning);
+ border-radius: var(--radius-sm);
+ transition: all 0.3s;
+}
+
+.update-notification.hidden {
+ display: none;
+}
+
+.notification-content {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.notification-icon {
+ font-size: 1.1em;
+}
+
+.notification-text {
+ flex: 1;
+ color: var(--text-primary);
+ font-size: 0.9em;
+}
+
+.dismiss-btn {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 1.2em;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+ transition: all 0.2s;
+}
+
+.dismiss-btn:hover {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+/* Loading */
+.loading-message {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--bg-tertiary);
+ border-top-color: var(--accent-primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto var(--space-md);
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes pulse {
+ from { opacity: 0.5; }
+ to { opacity: 1; }
+}
+
+/* Toast Notifications */
+.toast-stack {
+ position: fixed;
+ top: calc(var(--header-height) + 20px);
+ right: 20px;
+ width: min(420px, calc(100vw - 24px));
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ z-index: 12060;
+ pointer-events: none;
+}
+
+.toast,
+.ready-toast {
+ --toast-accent: 96, 165, 250;
+ pointer-events: auto;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ border-radius: 14px;
+ border: 1px solid rgba(var(--toast-accent), 0.42);
+ background:
+ linear-gradient(180deg, rgba(17, 25, 40, 0.95), rgba(9, 14, 24, 0.95));
+ box-shadow: 0 14px 32px rgba(2, 6, 16, 0.5);
+ color: rgba(244, 248, 255, 0.96);
+ padding: 11px 12px;
+ transform: translateX(20px);
+ opacity: 0;
+ transition: opacity 0.22s ease, transform 0.22s ease;
+}
+
+.toast.is-visible,
+.ready-toast.is-visible {
+ transform: translateX(0);
+ opacity: 1;
+}
+
+.toast.is-leaving,
+.ready-toast.is-leaving {
+ transform: translateX(16px);
+ opacity: 0;
+}
+
+.toast.toast-success,
+.ready-toast {
+ --toast-accent: 74, 222, 128;
+}
+
+.toast.toast-warning {
+ --toast-accent: 251, 191, 36;
+}
+
+.toast.toast-error {
+ --toast-accent: 248, 113, 113;
+}
+
+.toast-content {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ min-width: 0;
+ flex: 1;
+}
+
+.toast-icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(var(--toast-accent), 0.2);
+ color: rgb(var(--toast-accent));
+ flex: 0 0 auto;
+}
+
+.toast-icon svg {
+ width: 14px;
+ height: 14px;
+ display: block;
+}
+
+.toast-text {
+ font-weight: 500;
+ font-size: 0.88rem;
+ line-height: 1.38;
+ color: rgba(242, 247, 255, 0.95);
+ word-break: break-word;
+}
+
+.toast-close {
+ width: 22px;
+ height: 22px;
+ border-radius: 7px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ background: rgba(15, 23, 42, 0.58);
+ color: rgba(230, 237, 249, 0.84);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.72rem;
+ line-height: 1;
+ padding: 0;
+ flex: 0 0 auto;
+}
+
+.toast-close:hover {
+ background: rgba(51, 65, 85, 0.62);
+ color: rgba(255, 255, 255, 0.97);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+@keyframes slideInRight {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes fadeOutRight {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+}
+
+/* Responsive - ONLY apply if data-visible-count is not set */
+@media (max-width: 1200px) {
+ .terminal-grid:not([data-visible-count]) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .projects-chats-shell {
+ grid-template-columns: 1fr;
+ min-height: 70vh;
+ }
+
+ .sidebar-toggle {
+ display: inline-flex;
+ }
+
+ header {
+ padding: 0 var(--space-md);
+ }
+
+ .header-content {
+ gap: var(--space-md);
+ }
header h1 {
font-size: 1rem;
@@ -7438,25 +8201,26 @@ header h1 {
top: calc(var(--header-height) + 20px);
right: 20px;
width: 300px;
- z-index: 1001;
+ z-index: 12050;
pointer-events: none;
}
.cross-workspace-notification {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-left: 4px solid var(--accent-warning);
- border-radius: var(--radius-md);
+ background:
+ linear-gradient(180deg, rgba(18, 24, 38, 0.94), rgba(10, 15, 27, 0.94));
+ border: 1px solid rgba(245, 158, 11, 0.28);
+ border-left: 3px solid rgba(245, 158, 11, 0.85);
+ border-radius: 12px;
margin-bottom: var(--space-md);
- box-shadow: var(--shadow-soft);
+ box-shadow: 0 14px 28px rgba(2, 6, 16, 0.42);
pointer-events: auto;
- animation: slideInRight 0.3s ease-out;
+ animation: slideInRight 0.24s ease-out;
}
-.notification-header {
+.cross-workspace-notification .notification-header {
padding: var(--space-sm) var(--space-md);
- background: var(--bg-tertiary);
- border-bottom: 1px solid var(--border-color);
+ background: rgba(245, 158, 11, 0.1);
+ border-bottom: 1px solid rgba(245, 158, 11, 0.24);
display: flex;
justify-content: space-between;
align-items: center;
@@ -7468,7 +8232,7 @@ header h1 {
color: var(--text-primary);
}
-.notification-close {
+.cross-workspace-notification .notification-close {
background: none;
border: none;
color: var(--text-secondary);
@@ -7481,17 +8245,17 @@ header h1 {
justify-content: center;
}
-.notification-body {
+.cross-workspace-notification .notification-body {
padding: var(--space-md);
}
-.notification-message {
+.cross-workspace-notification .notification-message {
font-size: 0.9rem;
color: var(--text-primary);
margin-bottom: var(--space-md);
}
-.notification-actions {
+.cross-workspace-notification .notification-actions {
display: flex;
gap: var(--space-sm);
}
@@ -9260,395 +10024,661 @@ header h1 {
font-size: 0.75rem;
}
-.detail-row-full {
+.detail-row-full {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: flex-start;
+}
+
+.detail-row-full .detail-label {
+ color: var(--text-muted);
+ flex-shrink: 0;
+ min-width: 80px;
+}
+
+.folder-path-full {
+ color: #90cdf4;
+ word-break: break-all;
+ font-family: monospace;
+ font-size: 0.75rem;
+ background: rgba(99, 179, 237, 0.1);
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+/* Conversation details grid */
+.conv-details-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-xs) var(--space-md);
+ margin: var(--space-sm) 0;
+ padding: var(--space-sm);
+ background: var(--bg-primary);
+ border-radius: var(--radius-xs);
+ font-size: 0.75rem;
+}
+
+.detail-row {
+ display: flex;
+ gap: var(--space-xs);
+ align-items: baseline;
+}
+
+.detail-label {
+ color: var(--text-muted);
+ min-width: 60px;
+ flex-shrink: 0;
+}
+
+.detail-value {
+ color: var(--text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.detail-value.folder-path {
+ color: #63b3ed;
+ cursor: help;
+}
+
+.detail-value.repo-name {
+ color: #9f7aea;
+}
+
+.detail-value.branch-name {
+ color: #48bb78;
+}
+
+.detail-value.model-name {
+ color: #f6ad55;
+}
+
+.conv-actions {
+ display: flex;
+ gap: var(--space-sm);
+ flex-wrap: wrap;
+ margin-top: var(--space-sm);
+}
+
+.conv-actions .btn-small {
+ padding: 6px 12px;
+ font-size: 0.8rem;
+}
+
+.conv-actions .btn-small.primary {
+ background: var(--accent-primary);
+ color: var(--text-on-accent);
+ border: none;
+}
+
+.conv-actions .btn-small.primary:hover {
+ background: var(--accent-primary-hover);
+}
+
+.conv-actions .btn-small.secondary {
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+}
+
+.conv-actions .btn-small.secondary:hover {
+ background: var(--bg-tertiary);
+}
+
+.browser-footer {
+ padding: var(--space-sm) var(--space-lg);
+ border-top: 1px solid var(--border-color);
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+
+.no-results, .loading, .error {
+ text-align: center;
+ padding: var(--space-xl);
+ color: var(--text-muted);
+}
+
+/* Conversation Details Modal */
+.conversation-details-modal .details-content {
+ max-width: 700px;
+ width: 90vw;
+ max-height: 85vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.details-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.details-meta {
+ padding: var(--space-md) var(--space-lg);
+ background: var(--bg-tertiary);
+ font-size: 0.85rem;
+}
+
+.details-meta p {
+ margin: var(--space-xs) 0;
+}
+
+.details-summary {
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ font-style: italic;
+ color: var(--text-secondary);
+}
+
+.details-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-md) var(--space-lg);
+}
+
+.details-messages h4 {
+ margin-bottom: var(--space-md);
+}
+
+.messages-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.messages-list .message {
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+}
+
+.messages-list .message.user {
+ background: var(--bg-tertiary);
+ margin-left: var(--space-lg);
+}
+
+.messages-list .message.assistant {
+ background: var(--accent-primary);
+ background: rgba(59, 130, 246, 0.2);
+ margin-right: var(--space-lg);
+}
+
+.message-header {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-bottom: var(--space-xs);
+}
+
+.message-content {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.tool-uses {
+ margin-top: var(--space-xs);
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+
+.more-messages {
+ text-align: center;
+ padding: var(--space-md);
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+.details-actions {
+ padding: var(--space-md) var(--space-lg);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+}
+
+/* ============================================
+ Ports Panel Styles
+ ============================================ */
+
+.ports-modal .ports-content {
+ max-width: 1100px;
+ width: 96vw;
+ max-height: 92vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.ports-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.ports-header h2 {
+ margin: 0;
+ font-size: 1.2rem;
+}
+
+.ports-info {
+ padding: var(--space-sm) var(--space-lg);
+ background: var(--bg-tertiary);
+ font-size: 0.85rem;
+ color: var(--text-muted);
+}
+
+.ports-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-lg);
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+ gap: var(--space-md);
+ align-content: start;
+}
+
+.port-item {
display: flex;
+ flex-direction: column;
gap: var(--space-sm);
- align-items: flex-start;
+ padding: var(--space-md);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--border-color);
+ transition: all 0.15s;
}
-.detail-row-full .detail-label {
- color: var(--text-muted);
- flex-shrink: 0;
- min-width: 80px;
+.port-item:hover {
+ background: var(--bg-primary);
}
-.folder-path-full {
- color: #90cdf4;
- word-break: break-all;
- font-family: monospace;
- font-size: 0.75rem;
- background: rgba(99, 179, 237, 0.1);
- padding: 4px 8px;
- border-radius: 4px;
+.port-item.orchestrator,
+.port-item.orchestrator-dev {
+ border-left-color: #9f7aea;
}
-/* Conversation details grid */
-.conv-details-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: var(--space-xs) var(--space-md);
- margin: var(--space-sm) 0;
- padding: var(--space-sm);
- background: var(--bg-primary);
- border-radius: var(--radius-xs);
- font-size: 0.75rem;
+.port-item.client,
+.port-item.client-dev {
+ border-left-color: #63b3ed;
}
-.detail-row {
- display: flex;
- gap: var(--space-xs);
- align-items: baseline;
+.port-item.vite,
+.port-item.react {
+ border-left-color: #48bb78;
}
-.detail-label {
- color: var(--text-muted);
- min-width: 60px;
- flex-shrink: 0;
+.port-item.game-server {
+ border-left-color: #f6ad55;
}
-.detail-value {
- color: var(--text-secondary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.port-item.python,
+.port-item.flask {
+ border-left-color: #ffd93d;
}
-.detail-value.folder-path {
- color: #63b3ed;
- cursor: help;
+.port-item.node {
+ border-left-color: #68d391;
}
-.detail-value.repo-name {
- color: #9f7aea;
+.port-icon {
+ font-size: 1.2rem;
+ flex-shrink: 0;
}
-.detail-value.branch-name {
- color: #48bb78;
+.port-card-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-md);
+ min-width: 0;
}
-.detail-value.model-name {
- color: #f6ad55;
+.port-main {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-md);
+ min-width: 0;
+ flex: 1;
}
-.conv-actions {
- display: flex;
- gap: var(--space-sm);
- flex-wrap: wrap;
- margin-top: var(--space-sm);
+.port-details {
+ flex: 1;
+ min-width: 0;
}
-.conv-actions .btn-small {
- padding: 6px 12px;
- font-size: 0.8rem;
+.port-name {
+ display: block;
+ font-weight: 500;
+ color: var(--text-primary);
+ font-size: 0.9rem;
}
-.conv-actions .btn-small.primary {
- background: var(--accent-primary);
- color: var(--text-on-accent);
- border: none;
+.port-process {
+ font-size: 0.75rem;
+ color: var(--text-muted);
}
-.conv-actions .btn-small.primary:hover {
- background: var(--accent-primary-hover);
+.port-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--space-xs);
+ flex-wrap: wrap;
}
-.conv-actions .btn-small.secondary {
- background: var(--bg-primary);
+.port-action-btn {
border: 1px solid var(--border-color);
- color: var(--text-secondary);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ border-radius: var(--radius-xs);
+ padding: 4px 8px;
+ font-size: 0.75rem;
+ cursor: pointer;
+ line-height: 1.1;
+ transition: background 0.15s, border-color 0.15s;
}
-.conv-actions .btn-small.secondary:hover {
- background: var(--bg-tertiary);
+.port-action-btn:hover {
+ background: var(--bg-secondary);
+ border-color: var(--accent-primary);
}
-.browser-footer {
- padding: var(--space-sm) var(--space-lg);
+.ports-footer {
+ padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border-color);
- font-size: 0.8rem;
- color: var(--text-muted);
+ display: flex;
+ justify-content: flex-end;
}
-.no-results, .loading, .error {
+.no-ports {
text-align: center;
- padding: var(--space-xl);
+ padding: var(--space-lg);
color: var(--text-muted);
}
-/* Conversation Details Modal */
-.conversation-details-modal .details-content {
- max-width: 700px;
- width: 90vw;
- max-height: 85vh;
+/* ============================================
+ PRs Panel Styles
+ ============================================ */
+
+.prs-modal .prs-content {
+ max-width: 1100px;
+ width: 96vw;
+ max-height: 92vh;
display: flex;
flex-direction: column;
}
-.details-header {
+.prs-toolbar {
+ padding: var(--space-sm) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
display: flex;
- justify-content: space-between;
+ gap: var(--space-md);
align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
+ flex-wrap: wrap;
}
-.details-meta {
- padding: var(--space-md) var(--space-lg);
- background: var(--bg-tertiary);
- font-size: 0.85rem;
+.prs-toolbar-group {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
}
-.details-meta p {
- margin: var(--space-xs) 0;
+.prs-label {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
}
-.details-summary {
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
- font-style: italic;
- color: var(--text-secondary);
+.prs-search {
+ min-width: 220px;
+ flex: 1;
}
-.details-messages {
+.prs-input {
+ min-width: 220px;
+ flex: 0 1 320px;
+}
+
+.prs-list {
flex: 1;
overflow-y: auto;
- padding: var(--space-md) var(--space-lg);
+ padding: var(--space-lg);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
}
-.details-messages h4 {
- margin-bottom: var(--space-md);
+.pr-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+ padding: var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
}
-.messages-list {
+.pr-main {
display: flex;
flex-direction: column;
- gap: var(--space-sm);
+ gap: 6px;
+ min-width: 0;
+ flex: 1;
}
-.messages-list .message {
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
+.pr-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-.messages-list .message.user {
- background: var(--bg-tertiary);
- margin-left: var(--space-lg);
+.pr-repo {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.messages-list .message.assistant {
- background: var(--accent-primary);
- background: rgba(59, 130, 246, 0.2);
- margin-right: var(--space-lg);
+.pr-number {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.message-header {
- display: flex;
- justify-content: space-between;
+.pr-badge {
+ font-family: var(--font-mono);
font-size: 0.75rem;
- color: var(--text-muted);
- margin-bottom: var(--space-xs);
+ padding: 3px 6px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.message-content {
- white-space: pre-wrap;
- word-break: break-word;
+.pr-badge.draft {
+ border-color: var(--accent-warning);
}
-.tool-uses {
- margin-top: var(--space-xs);
- font-size: 0.7rem;
- color: var(--text-muted);
+.pr-subtitle {
+ font-weight: 600;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.more-messages {
- text-align: center;
- padding: var(--space-md);
- color: var(--text-muted);
- font-style: italic;
+.pr-meta {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
}
-.details-actions {
- padding: var(--space-md) var(--space-lg);
- border-top: 1px solid var(--border-color);
+.pr-actions {
display: flex;
- justify-content: flex-end;
+ gap: var(--space-sm);
+ flex-shrink: 0;
}
/* ============================================
- Ports Panel Styles
+ Tasks Panel Styles
============================================ */
-.ports-modal .ports-content {
- max-width: 1100px;
- width: 96vw;
- max-height: 92vh;
- display: flex;
- flex-direction: column;
-}
-
-.ports-header {
+.queue-summary {
display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
-}
-
-.ports-header h2 {
- margin: 0;
- font-size: 1.2rem;
-}
-
-.ports-info {
- padding: var(--space-sm) var(--space-lg);
- background: var(--bg-tertiary);
- font-size: 0.85rem;
- color: var(--text-muted);
-}
-
-.ports-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-lg);
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
- gap: var(--space-md);
- align-content: start;
+ gap: 8px;
+ flex-wrap: wrap;
+ padding: 0 0 10px 0;
}
-.port-item {
+.tasks-modal .tasks-content {
+ /* Tasks is a primary workflow: prefer a robust, full-viewport panel. */
+ max-width: none;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100vw;
+ max-height: 100vh;
+ height: 100%;
display: flex;
flex-direction: column;
- gap: var(--space-sm);
+ overflow: hidden;
+ position: relative;
+ border-radius: 0;
padding: var(--space-md);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- border-left: 3px solid var(--border-color);
- transition: all 0.15s;
+ --tasks-board-accent: var(--accent-primary);
+ direction: ltr;
+ text-align: left;
}
-.port-item:hover {
- background: var(--bg-primary);
+.tasks-modal .tasks-content.tasks-has-board-accent .tasks-toolbar {
+ border-bottom-color: color-mix(in srgb, var(--border-color) 55%, var(--tasks-board-accent) 45%);
}
-.port-item.orchestrator,
-.port-item.orchestrator-dev {
- border-left-color: #9f7aea;
+.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header {
+ background: color-mix(in srgb, var(--bg-tertiary) 86%, var(--tasks-board-accent) 14%);
}
-.port-item.client,
-.port-item.client-dev {
- border-left-color: #63b3ed;
+.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header:hover {
+ background: color-mix(in srgb, var(--bg-primary) 86%, var(--tasks-board-accent) 14%);
}
-.port-item.vite,
-.port-item.react {
- border-left-color: #48bb78;
+.tasks-modal .tasks-body {
+ direction: ltr;
+ text-align: left;
}
-.port-item.game-server {
- border-left-color: #f6ad55;
+.tasks-modal {
+ background: transparent;
}
-.port-item.python,
-.port-item.flask {
- border-left-color: #ffd93d;
+.tasks-modal.tasks-theme-light {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f6f8fa;
+ --bg-tertiary: #e6e8eb;
+ --text-primary: #24292f;
+ --text-secondary: #57606a;
+ --text-tertiary: #6e7781;
+ --border-color: #d0d7de;
}
-.port-item.node {
- border-left-color: #68d391;
+.tasks-modal .modal-header {
+ position: sticky;
+ top: 0;
+ z-index: 5;
+ background: var(--bg-secondary);
+ padding-bottom: var(--space-sm);
}
-.port-icon {
- font-size: 1.2rem;
- flex-shrink: 0;
+.tasks-modal .tasks-close-btn {
+ width: 44px;
+ height: 44px;
+ font-size: 1.6rem;
+ color: var(--accent-danger);
+ border: 1px solid rgba(248, 81, 73, 0.35);
+ background: rgba(248, 81, 73, 0.12);
}
-.port-card-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: var(--space-md);
- min-width: 0;
+.tasks-modal .tasks-close-btn:hover {
+ background: rgba(248, 81, 73, 0.2);
+ border-color: rgba(248, 81, 73, 0.55);
+ color: var(--accent-danger);
}
-.port-main {
- display: flex;
- align-items: flex-start;
- gap: var(--space-md);
- min-width: 0;
- flex: 1;
+.tasks-modal .tasks-close-btn:focus-visible {
+ outline: 2px solid var(--accent-danger);
+ outline-offset: 2px;
}
-.port-details {
- flex: 1;
- min-width: 0;
+.tasks-view-toggle {
+ display: flex;
+ gap: 6px;
+ align-items: center;
}
-.port-name {
- display: block;
- font-weight: 500;
- color: var(--text-primary);
- font-size: 0.9rem;
+.tasks-view-btn.active {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
}
-.port-process {
- font-size: 0.75rem;
- color: var(--text-muted);
+.tasks-filter {
+ position: relative;
}
-.port-actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: var(--space-xs);
- flex-wrap: wrap;
+.tasks-filter summary {
+ list-style: none;
}
-.port-action-btn {
- border: 1px solid var(--border-color);
- background: var(--bg-primary);
- color: var(--text-primary);
- border-radius: var(--radius-xs);
- padding: 4px 8px;
- font-size: 0.75rem;
- cursor: pointer;
- line-height: 1.1;
- transition: background 0.15s, border-color 0.15s;
+.tasks-filter summary::-webkit-details-marker {
+ display: none;
}
-.port-action-btn:hover {
+.tasks-filter-popover {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0;
+ min-width: 240px;
+ max-height: 320px;
+ overflow: auto;
+ z-index: 5;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
- border-color: var(--accent-primary);
+ box-shadow: 0 10px 24px rgba(0,0,0,0.12);
+ padding: 10px;
}
-.ports-footer {
- padding: var(--space-md) var(--space-lg);
- border-top: 1px solid var(--border-color);
+.tasks-filter-actions {
display: flex;
- justify-content: flex-end;
+ gap: 8px;
+ margin-bottom: 10px;
}
-.no-ports {
- text-align: center;
- padding: var(--space-lg);
- color: var(--text-muted);
+.tasks-filter-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
-/* ============================================
- PRs Panel Styles
- ============================================ */
-
-.prs-modal .prs-content {
- max-width: 1100px;
- width: 96vw;
- max-height: 92vh;
+.tasks-filter-item {
display: flex;
- flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+}
+
+.tasks-filter-item input {
+ accent-color: var(--accent-primary);
}
-.prs-toolbar {
+.tasks-toolbar {
padding: var(--space-sm) var(--space-lg);
border-bottom: 1px solid var(--border-color);
background: var(--bg-tertiary);
@@ -9658,201 +10688,255 @@ header h1 {
flex-wrap: wrap;
}
-.prs-toolbar-group {
- display: flex;
- gap: 8px;
+.tasks-launch-defaults {
+ display: inline-flex;
align-items: center;
- flex-wrap: wrap;
+ gap: 6px;
+ padding: 4px 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 999px;
+ background: var(--bg-secondary);
}
-.prs-label {
- font-size: 0.75rem;
- color: var(--text-secondary);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
+.tasks-launch-default-tier-group {
+ padding: 0;
+ border: none;
+ background: transparent;
}
-.prs-search {
- min-width: 220px;
- flex: 1;
+.tasks-launch-default-agent-group {
+ padding: 0;
+ border: none;
+ background: transparent;
}
-.prs-input {
- min-width: 220px;
- flex: 0 1 320px;
+.tasks-launch-default-mode-group {
+ padding: 0;
+ border: none;
+ background: transparent;
}
-.prs-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-lg);
- display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+.tasks-launch-defaults-label {
+ font-weight: 900;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
}
-.pr-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-md);
- padding: var(--space-md);
+.tasks-toggle.tasks-toggle-mini {
+ font-size: 0.75rem;
+ gap: 6px;
+}
+
+.tasks-toggle.tasks-toggle-mini span {
+ opacity: 0.9;
+}
+
+.tasks-board-accent {
+ width: 12px;
+ height: 12px;
+ border-radius: 999px;
+ background: var(--tasks-board-accent);
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.12);
}
-.pr-main {
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-width: 0;
- flex: 1;
+.tasks-board-accent.is-hidden {
+ display: none;
}
-.pr-title {
- display: flex;
+.tasks-board-picker {
+ position: relative;
+ display: inline-flex;
align-items: center;
gap: 8px;
- flex-wrap: wrap;
}
-.pr-repo {
- font-family: var(--font-mono);
- font-size: 0.8rem;
- color: var(--text-secondary);
+.tasks-board-btn {
+ min-width: 200px;
+ text-align: left;
+ justify-content: flex-start;
}
-.pr-number {
- font-family: var(--font-mono);
- font-size: 0.8rem;
- color: var(--text-secondary);
+.tasks-select-hidden {
+ position: absolute;
+ left: -9999px;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ pointer-events: none;
}
-.pr-badge {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- padding: 3px 6px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
+.tasks-board-menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 0;
+ z-index: 10;
+ min-width: 320px;
+ max-width: 420px;
+ max-height: 60vh;
+ overflow: auto;
background: var(--bg-tertiary);
- color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
+ padding: 6px;
}
-.pr-badge.draft {
- border-color: var(--accent-warning);
+.tasks-board-menu-search-wrap {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: var(--bg-tertiary);
+ padding: 6px;
+ border-bottom: 1px solid var(--border-color);
+ margin: -6px -6px 6px;
}
-.pr-subtitle {
- font-weight: 600;
+.tasks-board-menu-search {
+ width: 100%;
+ background: var(--bg-secondary);
color: var(--text-primary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 8px 10px;
+ font-size: 0.85rem;
}
-.pr-meta {
- font-size: 0.75rem;
- color: var(--text-tertiary);
+.tasks-board-menu-search:focus-visible {
+ outline: 2px solid var(--accent-primary);
+ outline-offset: 2px;
}
-.pr-actions {
+.tasks-board-menu.hidden {
+ display: none;
+}
+
+.tasks-board-menu-item {
+ width: 100%;
display: flex;
- gap: var(--space-sm);
- flex-shrink: 0;
+ align-items: center;
+ gap: 10px;
+ background: transparent;
+ border: 1px solid transparent;
+ color: var(--text-primary);
+ border-radius: var(--radius-md);
+ padding: 8px 10px;
+ cursor: pointer;
+ text-align: left;
}
-/* ============================================
- Tasks Panel Styles
- ============================================ */
+.tasks-board-menu-item:hover {
+ background: rgba(31, 111, 235, 0.12);
+ border-color: rgba(31, 111, 235, 0.25);
+}
-.queue-summary {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- padding: 0 0 10px 0;
+.tasks-board-menu-item.is-active {
+ border-color: rgba(31, 111, 235, 0.5);
}
-.tasks-modal .tasks-content {
- /* Tasks is a primary workflow: prefer a robust, full-viewport panel. */
- max-width: none;
- box-sizing: border-box;
- width: 100%;
- max-width: 100vw;
- max-height: 100vh;
- height: 100%;
- display: flex;
- flex-direction: column;
+.tasks-board-menu-item.is-selected {
+ background: rgba(31, 111, 235, 0.18);
+ border-color: rgba(31, 111, 235, 0.35);
+}
+
+.tasks-board-menu-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ flex: 0 0 auto;
+}
+
+.tasks-board-menu-dot.is-hidden {
+ visibility: hidden;
+}
+
+.tasks-board-menu-label {
overflow: hidden;
- position: relative;
- border-radius: 0;
- padding: var(--space-md);
- --tasks-board-accent: var(--accent-primary);
- direction: ltr;
- text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.tasks-modal .tasks-content.tasks-has-board-accent .tasks-toolbar {
- border-bottom-color: color-mix(in srgb, var(--border-color) 55%, var(--tasks-board-accent) 45%);
+.tasks-board-menu-empty {
+ padding: 10px 12px;
+ color: var(--text-tertiary);
+ font-size: 0.85rem;
}
-.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header {
- background: color-mix(in srgb, var(--bg-tertiary) 86%, var(--tasks-board-accent) 14%);
+.tasks-hotkeys-overlay {
+ position: absolute;
+ inset: 0;
+ background: var(--overlay-backdrop);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ z-index: 20;
}
-.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header:hover {
- background: color-mix(in srgb, var(--bg-primary) 86%, var(--tasks-board-accent) 14%);
+.tasks-launch-popover-overlay {
+ position: absolute;
+ inset: 0;
+ background: var(--overlay-backdrop-faint);
+ z-index: 19;
+}
+
+.tasks-launch-popover {
+ position: absolute;
+ width: min(460px, 92vw);
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-float);
+ padding: 12px;
}
-.tasks-modal .tasks-body {
- direction: ltr;
- text-align: left;
+.tasks-launch-popover-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 8px;
}
-.tasks-modal {
- background: transparent;
+.tasks-launch-popover-title {
+ font-weight: 800;
+ color: var(--text-primary);
}
-.tasks-modal.tasks-theme-light {
- --bg-primary: #ffffff;
- --bg-secondary: #f6f8fa;
- --bg-tertiary: #e6e8eb;
- --text-primary: #24292f;
- --text-secondary: #57606a;
- --text-tertiary: #6e7781;
- --border-color: #d0d7de;
+.tasks-launch-popover-meta {
+ font-size: 0.8rem;
+ color: var(--text-tertiary);
+ margin-bottom: 10px;
}
-.tasks-modal .modal-header {
- position: sticky;
- top: 0;
- z-index: 5;
+.tasks-launch-popover-warn {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 12px;
background: var(--bg-secondary);
- padding-bottom: var(--space-sm);
-}
-
-.tasks-modal .tasks-close-btn {
- width: 44px;
- height: 44px;
- font-size: 1.6rem;
- color: var(--accent-danger);
- border: 1px solid rgba(248, 81, 73, 0.35);
- background: rgba(248, 81, 73, 0.12);
+ margin-bottom: 10px;
}
-.tasks-modal .tasks-close-btn:hover {
- background: rgba(248, 81, 73, 0.2);
- border-color: rgba(248, 81, 73, 0.55);
- color: var(--accent-danger);
+.tasks-launch-popover-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ align-items: end;
}
-.tasks-modal .tasks-close-btn:focus-visible {
- outline: 2px solid var(--accent-danger);
- outline-offset: 2px;
+.tasks-launch-popover-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.tasks-view-toggle {
+.tasks-launch-popover-actions {
display: flex;
gap: 6px;
align-items: center;
@@ -9860,9 +10944,15 @@ header h1 {
max-width: 100%;
}
-.tasks-view-btn.active {
- border-color: var(--accent-primary);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
+.tasks-hotkeys-card {
+ width: min(860px, 96vw);
+ max-height: min(80vh, 760px);
+ overflow: auto;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
+ padding: 14px;
}
.tasks-view-btn {
@@ -9873,50 +10963,58 @@ header h1 {
position: relative;
}
-.tasks-filter summary {
- list-style: none;
+.tasks-hotkeys-title {
+ font-weight: 800;
+ font-size: 1rem;
+ color: var(--text-primary);
}
-.tasks-filter summary::-webkit-details-marker {
- display: none;
+.tasks-hotkeys-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
}
-.tasks-filter-popover {
- position: absolute;
- top: calc(100% + 8px);
- left: 0;
- min-width: 240px;
- max-height: 320px;
- overflow: auto;
- z-index: 5;
+.tasks-hotkeys-group {
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
background: var(--bg-secondary);
- box-shadow: 0 10px 24px rgba(0,0,0,0.12);
+ border-radius: var(--radius-md);
padding: 10px;
}
-.tasks-filter-actions {
- display: flex;
- gap: 8px;
- margin-bottom: 10px;
+.tasks-hotkeys-group-title {
+ font-weight: 800;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ margin-bottom: 8px;
}
-.tasks-filter-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.tasks-hotkeys-row {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ line-height: 1.3;
+ margin-bottom: 6px;
}
-.tasks-filter-item {
- display: flex;
+.tasks-hotkeys-row code {
+ font-size: 0.8rem;
+ background: rgba(31, 111, 235, 0.12);
+ border: 1px solid rgba(31, 111, 235, 0.2);
+ border-radius: 6px;
+ padding: 2px 6px;
+ color: var(--text-primary);
+}
+
+.tasks-toggle {
+ display: inline-flex;
align-items: center;
gap: 8px;
- font-size: 0.85rem;
- color: var(--text-primary);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ user-select: none;
}
-.tasks-filter-item input {
+.tasks-toggle input {
accent-color: var(--accent-primary);
}
@@ -9927,7 +11025,11 @@ header h1 {
display: flex;
gap: var(--space-sm);
align-items: center;
- flex-wrap: wrap;
+ gap: 6px;
+ padding: 4px 6px;
+ border: 1px solid var(--border-color);
+ border-radius: 999px;
+ background: var(--bg-secondary);
}
#queue-panel .tasks-toolbar {
@@ -9999,389 +11101,460 @@ header h1 {
align-items: center;
gap: 6px;
padding: 4px 8px;
- border: 1px solid var(--border-color);
border-radius: 999px;
- background: var(--bg-secondary);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ user-select: none;
}
-.tasks-launch-default-tier-group {
- padding: 0;
- border: none;
- background: transparent;
+.tasks-radio-option input {
+ margin: 0;
+ accent-color: var(--accent-primary);
}
-.tasks-launch-default-agent-group {
- padding: 0;
- border: none;
- background: transparent;
+.tasks-radio-option:has(input:checked) {
+ background: rgba(31, 111, 235, 0.15);
+ color: var(--text-primary);
}
-.tasks-launch-default-mode-group {
- padding: 0;
- border: none;
- background: transparent;
+.tasks-radio-option:has(input:focus-visible) {
+ outline: 2px solid var(--accent-primary);
+ outline-offset: 2px;
}
-.tasks-launch-defaults-label {
- font-weight: 900;
+.tasks-select {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 6px 10px;
font-size: 0.85rem;
- color: var(--text-secondary);
+ min-width: 160px;
}
-.tasks-toggle.tasks-toggle-mini {
- font-size: 0.75rem;
- gap: 6px;
+.tasks-search {
+ min-width: 220px;
+ flex: 1;
}
-.tasks-toggle.tasks-toggle-mini span {
- opacity: 0.9;
+.tasks-body {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: 420px 1fr;
+ grid-template-rows: 1fr;
+ grid-template-areas: "cards detail";
}
-.tasks-board-accent {
- width: 12px;
- height: 12px;
- border-radius: 999px;
- background: var(--tasks-board-accent);
- border: 1px solid var(--border-color);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.12);
+.tasks-body.tasks-body-board {
+ /* Board view: show full-width kanban until a card is selected. */
+ grid-template-columns: 1fr;
+ grid-template-areas: "cards";
+ position: relative;
+ overflow: hidden;
+}
+
+.tasks-body.tasks-body-board .tasks-detail {
+ display: none;
+}
+
+.tasks-body.tasks-body-board .tasks-cards {
+ grid-column: 1;
+ border-left: none;
+}
+
+.tasks-body.tasks-body-board.tasks-kanban-wrap .tasks-cards {
+ overflow: auto;
+}
+
+.tasks-body.tasks-body-board.tasks-has-detail {
+ /* When a card is selected, show details as an overlay on the right. */
+ grid-template-columns: 1fr;
+}
+
+.tasks-body.tasks-body-board.tasks-has-detail .tasks-detail {
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 520px;
+ max-width: min(520px, 65vw);
+ border-left: 1px solid var(--border-color);
+ box-shadow: -10px 0 24px rgba(0, 0, 0, 0.25);
+ z-index: 5;
+}
+
+.tasks-body.tasks-body-board.tasks-has-detail .tasks-cards {
+ grid-column: 1;
+ border-left: none;
+}
+
+.tasks-cards {
+ grid-area: cards;
+ grid-column: 1;
+ grid-row: 1;
+ overflow-y: auto;
+ padding: var(--space-md);
+ border-right: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ min-width: 0;
}
-.tasks-board-accent.is-hidden {
- display: none;
+.tasks-body.tasks-body-board .tasks-cards {
+ border-right: none;
+ overflow: hidden;
+ padding: var(--space-sm);
+ background: var(--bg-primary);
}
-.tasks-board-picker {
- position: relative;
- display: inline-flex;
- align-items: center;
- gap: 8px;
+.tasks-board {
+ height: 100%;
+ display: flex;
+ gap: var(--space-md);
+ justify-content: flex-start;
+ overflow-x: auto;
+ overflow-y: hidden;
+ padding-bottom: var(--space-sm);
+ scroll-snap-type: x mandatory;
}
-.tasks-board-btn {
- min-width: 200px;
- text-align: left;
- justify-content: flex-start;
+.tasks-board.tasks-board-wrap {
+ flex-wrap: wrap;
+ overflow-x: hidden;
+ overflow-y: auto;
+ align-content: flex-start;
+ scroll-snap-type: none;
}
-.tasks-select-hidden {
- position: absolute;
- left: -9999px;
- width: 1px;
- height: 1px;
- opacity: 0;
- pointer-events: none;
+.tasks-board.tasks-board-wrap.tasks-board-grid .tasks-column-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ align-content: start;
}
-.tasks-board-menu {
- position: absolute;
- top: calc(100% + 6px);
- left: 0;
- z-index: 10;
- min-width: 320px;
- max-width: 420px;
- max-height: 60vh;
- overflow: auto;
- background: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
- padding: 6px;
+.tasks-board.tasks-board-expand .tasks-column-cards {
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-rows: repeat(var(--tasks-card-rows, 1), min-content);
+ grid-template-columns: repeat(var(--tasks-card-columns, 1), minmax(180px, 1fr));
+ align-content: start;
+ gap: var(--space-sm);
+ overflow: hidden;
+ flex: 1;
+ min-height: 0;
}
-.tasks-board-menu-search-wrap {
- position: sticky;
- top: 0;
- z-index: 1;
- background: var(--bg-tertiary);
- padding: 6px;
- border-bottom: 1px solid var(--border-color);
- margin: -6px -6px 6px;
+.tasks-board.tasks-board-grid .task-card-board {
+ height: fit-content;
}
-.tasks-board-menu-search {
- width: 100%;
+.tasks-column {
+ --tasks-col-expanded: clamp(240px, 22vw, 360px);
+ --tasks-col-collapsed: 56px;
+ --tasks-card-columns: 1;
+ --tasks-card-rows: 1;
+ width: var(--tasks-col-expanded);
+ min-width: var(--tasks-col-expanded);
background: var(--bg-secondary);
- color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- padding: 8px 10px;
- font-size: 0.85rem;
-}
-
-.tasks-board-menu-search:focus-visible {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ scroll-snap-align: start;
+ position: relative;
}
-.tasks-board-menu.hidden {
- display: none;
+.tasks-column.hover {
+ border-color: var(--tasks-board-accent);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
}
-.tasks-board-menu-item {
- width: 100%;
+.tasks-column-header {
+ padding: var(--space-sm) var(--space-md);
display: flex;
- align-items: center;
- gap: 10px;
- background: transparent;
- border: 1px solid transparent;
- color: var(--text-primary);
- border-radius: var(--radius-md);
- padding: 8px 10px;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--space-sm);
+ width: 100%;
+ background: var(--bg-tertiary);
+ border: none;
+ border-bottom: 1px solid var(--border-color);
cursor: pointer;
text-align: left;
+ color: var(--text-primary);
}
-.tasks-board-menu-item:hover {
- background: rgba(31, 111, 235, 0.12);
- border-color: rgba(31, 111, 235, 0.25);
-}
-
-.tasks-board-menu-item.is-active {
- border-color: rgba(31, 111, 235, 0.5);
-}
-
-.tasks-board-menu-item.is-selected {
- background: rgba(31, 111, 235, 0.18);
- border-color: rgba(31, 111, 235, 0.35);
-}
-
-.tasks-board-menu-dot {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
- flex: 0 0 auto;
+.tasks-column-header:hover {
+ background: var(--bg-primary);
}
-.tasks-board-menu-dot.is-hidden {
- visibility: hidden;
+.tasks-column-header:focus-visible {
+ outline: 2px solid var(--accent-primary);
+ outline-offset: 2px;
}
-.tasks-board-menu-label {
+.tasks-column-title {
+ font-weight: 700;
+ font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ color: var(--text-primary);
}
-.tasks-board-menu-empty {
- padding: 10px 12px;
+.tasks-column-count {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
color: var(--text-tertiary);
- font-size: 0.85rem;
}
-.tasks-hotkeys-overlay {
- position: absolute;
- inset: 0;
- background: var(--overlay-backdrop);
+.tasks-column-cards {
+ overflow-y: auto;
+ padding: var(--space-sm);
display: flex;
- align-items: center;
- justify-content: center;
- padding: 16px;
- z-index: 20;
-}
-
-.tasks-launch-popover-overlay {
- position: absolute;
- inset: 0;
- background: var(--overlay-backdrop-faint);
- z-index: 19;
+ flex-direction: column;
+ gap: var(--space-sm);
}
-.tasks-launch-popover {
- position: absolute;
- width: min(460px, 92vw);
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-float);
- padding: 12px;
+.tasks-column.is-collapsed {
+ width: var(--tasks-col-collapsed);
+ min-width: var(--tasks-col-collapsed);
}
-.tasks-launch-popover-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 8px;
+.tasks-column.is-collapsed .tasks-column-cards {
+ display: none;
}
-.tasks-launch-popover-title {
+.tasks-column.is-collapsed .tasks-column-title {
+ writing-mode: vertical-rl;
+ transform: rotate(180deg);
+ white-space: nowrap;
+ overflow: visible;
+ text-overflow: unset;
+ font-size: 0.9rem;
font-weight: 800;
- color: var(--text-primary);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
}
-.tasks-launch-popover-meta {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- margin-bottom: 10px;
+.tasks-column.is-collapsed .tasks-column-header {
+ justify-content: flex-start;
+ align-items: center;
+ flex-direction: column;
+ height: 100%;
+ padding: 10px 8px;
+ gap: 14px;
}
-.tasks-launch-popover-warn {
- font-size: 0.85rem;
- color: var(--text-secondary);
+.tasks-column.is-collapsed .tasks-column-count {
+ display: inline-flex;
+ min-width: 30px;
+ height: 30px;
+ padding: 0 10px;
+ border-radius: 999px;
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- padding: 10px 12px;
background: var(--bg-secondary);
- margin-bottom: 10px;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ color: var(--text-primary);
+ order: -1;
}
-.tasks-launch-popover-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 10px;
- align-items: end;
+.task-card-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 6px;
}
-.tasks-launch-popover-field {
- display: flex;
- flex-direction: column;
- gap: 6px;
- font-size: 0.8rem;
- color: var(--text-secondary);
+.task-card-top-right {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
}
-.tasks-launch-popover-actions {
- display: flex;
- gap: 10px;
- justify-content: flex-end;
- margin-top: 12px;
+.task-card-quick-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
}
-.tasks-hotkeys-card {
- width: min(860px, 96vw);
- max-height: min(80vh, 760px);
- overflow: auto;
- background: var(--bg-primary);
+.tasks-quick-tier-group {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px;
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
- padding: 14px;
+ border-radius: 999px;
+ background: var(--bg-secondary);
}
-.tasks-hotkeys-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 12px;
+.tasks-quick-tier-btn {
+ padding: 3px 7px;
+ font-size: 0.72rem;
+ font-weight: 900;
+ line-height: 1;
+ border-radius: 999px;
}
-.tasks-hotkeys-title {
- font-weight: 800;
- font-size: 1rem;
+.tasks-quick-tier-btn.is-selected {
+ border-color: var(--accent-primary);
+ background: rgba(31, 111, 235, 0.18);
color: var(--text-primary);
}
-.tasks-hotkeys-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 12px;
+.tasks-select.tasks-select-mini {
+ min-width: 64px;
+ padding: 4px 6px;
+ font-size: 0.75rem;
+ font-weight: 800;
}
-.tasks-hotkeys-group {
+.tasks-quick-launch-btn {
+ padding: 4px 8px;
+ font-size: 0.8rem;
+ line-height: 1;
+}
+
+.task-card-labels {
+ display: inline-flex;
+ gap: 6px;
+ flex-wrap: nowrap;
+ overflow: hidden;
+ min-width: 0;
+}
+
+.tasks-label {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 6px;
+ border-radius: 999px;
+ font-size: 0.7rem;
+ line-height: 1.2;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
- border-radius: var(--radius-md);
- padding: 10px;
+ color: var(--text-secondary);
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.tasks-hotkeys-group-title {
- font-weight: 800;
- font-size: 0.85rem;
- color: var(--text-primary);
- margin-bottom: 8px;
+.tasks-label-editor {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
}
-.tasks-hotkeys-row {
- font-size: 0.85rem;
- color: var(--text-secondary);
- line-height: 1.3;
- margin-bottom: 6px;
+.tasks-label-toggle {
+ appearance: none;
+ cursor: pointer;
}
-.tasks-hotkeys-row code {
- font-size: 0.8rem;
- background: rgba(31, 111, 235, 0.12);
- border: 1px solid rgba(31, 111, 235, 0.2);
- border-radius: 6px;
- padding: 2px 6px;
- color: var(--text-primary);
+.tasks-label-toggle.is-selected {
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.25);
}
-.tasks-toggle {
+.tasks-checkbox {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--text-secondary);
- user-select: none;
}
-.tasks-toggle input {
+.tasks-checkbox input {
+ margin: 0;
accent-color: var(--accent-primary);
}
-.tasks-radio {
+.tasks-label--green { border-color: #2ea043; background: rgba(46, 160, 67, 0.12); color: var(--text-primary); }
+.tasks-label--yellow { border-color: #d29922; background: rgba(210, 153, 34, 0.14); color: var(--text-primary); }
+.tasks-label--orange { border-color: #f78166; background: rgba(247, 129, 102, 0.14); color: var(--text-primary); }
+.tasks-label--red { border-color: #f85149; background: rgba(248, 81, 73, 0.14); color: var(--text-primary); }
+.tasks-label--purple { border-color: #a371f7; background: rgba(163, 113, 247, 0.14); color: var(--text-primary); }
+.tasks-label--blue { border-color: #1f6feb; background: rgba(31, 111, 235, 0.14); color: var(--text-primary); }
+.tasks-label--sky { border-color: #79c0ff; background: rgba(121, 192, 255, 0.14); color: var(--text-primary); }
+.tasks-label--lime { border-color: #7ee787; background: rgba(126, 231, 135, 0.14); color: var(--text-primary); }
+.tasks-label--pink { border-color: #ff80c8; background: rgba(255, 128, 200, 0.14); color: var(--text-primary); }
+.tasks-label--black { border-color: #30363d; background: rgba(48, 54, 61, 0.6); color: var(--text-primary); }
+.tasks-label--more { border-color: var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); }
+
+.task-card-assignees {
display: inline-flex;
- align-items: center;
gap: 6px;
- padding: 4px 6px;
- border: 1px solid var(--border-color);
- border-radius: 999px;
- background: var(--bg-secondary);
+ flex-shrink: 0;
}
-.tasks-radio-option {
+.tasks-avatar {
+ width: 22px;
+ height: 22px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-secondary);
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 4px 8px;
- border-radius: 999px;
- font-size: 0.8rem;
+ justify-content: center;
+ overflow: hidden;
color: var(--text-secondary);
- cursor: pointer;
- user-select: none;
+ font-size: 0.75rem;
+ text-decoration: none;
}
-.tasks-radio-option input {
- margin: 0;
- accent-color: var(--accent-primary);
+.tasks-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
}
-.tasks-radio-option:has(input:checked) {
- background: rgba(31, 111, 235, 0.15);
- color: var(--text-primary);
+.tasks-avatar-more {
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
}
-.tasks-radio-option:has(input:focus-visible) {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
+.task-card-due {
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ color: var(--text-secondary);
}
-.tasks-select {
- background: var(--bg-secondary);
- color: var(--text-primary);
+.tasks-kv {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.tasks-kv-row {
+ display: grid;
+ grid-template-columns: 140px 1fr;
+ gap: 10px;
+ padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- padding: 6px 10px;
- font-size: 0.85rem;
- min-width: 160px;
+ background: var(--bg-secondary);
}
-.tasks-search {
- min-width: 220px;
- flex: 1;
+.tasks-kv-row-edit {
+ align-items: center;
}
-.tasks-body {
- flex: 1;
- min-height: 0;
- display: grid;
- grid-template-columns: 420px 1fr;
- grid-template-rows: 1fr;
- grid-template-areas: "cards detail";
+.tasks-kv-key {
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ font-weight: 700;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
#queue-panel .tasks-body {
@@ -10407,106 +11580,139 @@ header h1 {
margin-top: var(--space-sm);
}
-.tasks-body.tasks-body-board {
- /* Board view: show full-width kanban until a card is selected. */
- grid-template-columns: 1fr;
- grid-template-areas: "cards";
- position: relative;
- overflow: hidden;
+.tasks-body.tasks-body-board {
+ /* Board view: show full-width kanban until a card is selected. */
+ grid-template-columns: 1fr;
+ grid-template-areas: "cards";
+ position: relative;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tasks-kv-val-edit {
+ overflow: visible;
+ text-overflow: unset;
+ white-space: normal;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.task-card-board {
+ cursor: grab;
+}
+
+.task-card-board.dragging {
+ opacity: 0.6;
+ cursor: grabbing;
+}
+
+.task-card-row {
+ padding: var(--space-sm) var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary);
+ cursor: pointer;
+ transition: border-color 0.15s, transform 0.15s;
+}
+
+.task-card-list {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.task-card-list-main {
+ min-width: 0;
+ flex: 1;
}
-.tasks-body.tasks-body-board .tasks-detail {
- display: none;
+.task-card-list-actions {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
}
-.tasks-body.tasks-body-board .tasks-cards {
- grid-column: 1;
- border-left: none;
+.task-card-row:hover {
+ border-color: var(--accent-primary);
+ transform: translateY(-1px);
}
-.tasks-body.tasks-body-board.tasks-kanban-wrap .tasks-cards {
- overflow: auto;
+.task-card-row.active {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
}
-.tasks-body.tasks-body-board.tasks-has-detail {
- /* When a card is selected, show details as an overlay on the right. */
- grid-template-columns: 1fr;
+.task-card-title {
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
}
-.tasks-body.tasks-body-board.tasks-has-detail .tasks-detail {
- display: block;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- width: 520px;
- max-width: min(520px, 65vw);
- border-left: 1px solid var(--border-color);
- box-shadow: -10px 0 24px rgba(0, 0, 0, 0.25);
- z-index: 5;
+.tasks-card-board-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ margin-right: 8px;
+ transform: translateY(1px);
}
-.tasks-body.tasks-body-board.tasks-has-detail .tasks-cards {
- grid-column: 1;
- border-left: none;
+.task-card-meta {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
}
-.tasks-cards {
- grid-area: cards;
- grid-column: 1;
+.tasks-detail {
+ grid-area: detail;
+ grid-column: 2;
grid-row: 1;
overflow-y: auto;
- padding: var(--space-md);
- border-right: 1px solid var(--border-color);
- background: var(--bg-secondary);
- display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+ padding: var(--space-lg);
+ background: var(--bg-primary);
min-width: 0;
}
-.tasks-body.tasks-body-board .tasks-cards {
- border-right: none;
- overflow: hidden;
- padding: var(--space-sm);
- background: var(--bg-primary);
+.tasks-detail-empty {
+ color: var(--text-secondary);
}
-.tasks-board {
- height: 100%;
+.tasks-detail-header {
display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
gap: var(--space-md);
- justify-content: flex-start;
- overflow-x: auto;
- overflow-y: hidden;
- padding-bottom: var(--space-sm);
- scroll-snap-type: x mandatory;
+ margin-bottom: var(--space-sm);
}
-.tasks-board.tasks-board-wrap {
- flex-wrap: wrap;
- overflow-x: hidden;
- overflow-y: auto;
- align-content: flex-start;
- scroll-snap-type: none;
+.tasks-detail-title {
+ font-weight: 700;
+ font-size: 1rem;
+ line-height: 1.2;
}
-.tasks-board.tasks-board-wrap.tasks-board-grid .tasks-column-cards {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
- align-content: start;
+.tasks-detail-actions {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ flex-shrink: 0;
}
-.tasks-board.tasks-board-expand .tasks-column-cards {
- display: grid;
- grid-auto-flow: column;
- grid-template-rows: repeat(var(--tasks-card-rows, 1), min-content);
- grid-template-columns: repeat(var(--tasks-card-columns, 1), minmax(180px, 1fr));
- align-content: start;
- gap: var(--space-sm);
- overflow: hidden;
- flex: 1;
- min-height: 0;
+.tasks-input {
+ width: 100%;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 8px 10px;
+ font-size: 0.95rem;
+ font-weight: 700;
}
.tasks-board.tasks-board-expand .task-card-title {
@@ -10522,1143 +11728,1163 @@ header h1 {
height: fit-content;
}
-.tasks-column {
- --tasks-col-expanded: clamp(240px, 22vw, 360px);
- --tasks-col-collapsed: 56px;
- --tasks-card-columns: 1;
- --tasks-card-rows: 1;
- width: var(--tasks-col-expanded);
- min-width: var(--tasks-col-expanded);
+.tasks-textarea {
+ width: 100%;
background: var(--bg-secondary);
+ color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- display: flex;
- flex-direction: column;
- max-height: 100%;
- scroll-snap-align: start;
- position: relative;
-}
-
-.tasks-column.hover {
- border-color: var(--tasks-board-accent);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
+ padding: 8px 10px;
+ font-size: 0.85rem;
+ font-family: var(--font-mono);
+ resize: vertical;
}
-.tasks-column-header {
- padding: var(--space-sm) var(--space-md);
+.tasks-inline-row {
display: flex;
- align-items: baseline;
- justify-content: space-between;
gap: var(--space-sm);
- width: 100%;
- background: var(--bg-tertiary);
- border: none;
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- text-align: left;
- color: var(--text-primary);
-}
-
-.tasks-column-header:hover {
- background: var(--bg-primary);
-}
-
-.tasks-column-header:focus-visible {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
-}
-
-.tasks-column-title {
- font-weight: 700;
- font-size: 0.9rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--text-primary);
-}
-
-.tasks-column-count {
- font-family: var(--font-mono);
- font-size: 0.8rem;
- color: var(--text-tertiary);
+ align-items: center;
+ flex-wrap: wrap;
}
-.tasks-column-cards {
- overflow-y: auto;
- padding: var(--space-sm);
+.tasks-combined-list {
display: flex;
flex-direction: column;
- gap: var(--space-sm);
-}
-
-.tasks-column.is-collapsed {
- width: var(--tasks-col-collapsed);
- min-width: var(--tasks-col-collapsed);
-}
-
-.tasks-column.is-collapsed .tasks-column-cards {
- display: none;
-}
-
-.tasks-column.is-collapsed .tasks-column-title {
- writing-mode: vertical-rl;
- transform: rotate(180deg);
- white-space: nowrap;
- overflow: visible;
- text-overflow: unset;
- font-size: 0.9rem;
- font-weight: 800;
- letter-spacing: 0.04em;
- text-transform: uppercase;
+ gap: 8px;
}
-.tasks-column.is-collapsed .tasks-column-header {
- justify-content: flex-start;
+.tasks-combined-item {
+ display: flex;
align-items: center;
- flex-direction: column;
- height: 100%;
- padding: 10px 8px;
- gap: 14px;
-}
-
-.tasks-column.is-collapsed .tasks-column-count {
- display: inline-flex;
- min-width: 30px;
- height: 30px;
- padding: 0 10px;
- border-radius: 999px;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 10px 12px;
border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
- align-items: center;
- justify-content: center;
- font-weight: 700;
- color: var(--text-primary);
- order: -1;
}
-.task-card-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- margin-bottom: 6px;
+.tasks-combined-label {
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.task-card-top-right {
+.tasks-combined-actions {
display: inline-flex;
- align-items: center;
- gap: 8px;
+ gap: 6px;
flex-shrink: 0;
}
-.task-card-quick-actions {
+.tasks-chips {
display: inline-flex;
- align-items: center;
gap: 6px;
+ flex-wrap: wrap;
+ margin-left: 6px;
}
-.tasks-quick-tier-group {
+.tasks-chip {
display: inline-flex;
align-items: center;
- gap: 4px;
- padding: 2px;
- border: 1px solid var(--border-color);
+ gap: 6px;
+ padding: 4px 8px;
border-radius: 999px;
+ border: 1px solid var(--border-color);
background: var(--bg-secondary);
+ font-size: 0.75rem;
+ color: var(--text-secondary);
}
-.tasks-quick-tier-btn {
- padding: 3px 7px;
- font-size: 0.72rem;
- font-weight: 900;
- line-height: 1;
+.tasks-chip-avatar {
+ width: 16px;
+ height: 16px;
border-radius: 999px;
+ object-fit: cover;
+ display: inline-block;
}
-.tasks-quick-tier-btn.is-selected {
- border-color: var(--accent-primary);
- background: rgba(31, 111, 235, 0.18);
+.tasks-chip-link {
+ color: var(--text-secondary);
+ text-decoration: none;
+}
+
+.tasks-chip-link:hover {
color: var(--text-primary);
+ text-decoration: underline;
}
-.tasks-select.tasks-select-mini {
- min-width: 64px;
- padding: 4px 6px;
- font-size: 0.75rem;
- font-weight: 800;
+.tasks-chip-muted {
+ opacity: 0.7;
}
-.tasks-quick-launch-btn {
- padding: 4px 8px;
- font-size: 0.8rem;
+.tasks-chip-x {
+ appearance: none;
+ border: none;
+ background: transparent;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ font-size: 0.9rem;
line-height: 1;
+ padding: 0 2px;
}
-.task-card-labels {
- display: inline-flex;
- gap: 6px;
- flex-wrap: nowrap;
- overflow: hidden;
- min-width: 0;
+.tasks-chip-x:hover {
+ color: var(--text-primary);
}
-.tasks-label {
- display: inline-flex;
+.tasks-detail-block {
+ margin-top: var(--space-md);
+}
+
+.tasks-detail-block.tasks-dropzone-hover {
+ outline: 2px dashed var(--accent-color);
+ outline-offset: 6px;
+ border-radius: var(--radius-md);
+}
+
+.tasks-detail-block-title {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ margin-bottom: 6px;
+}
+
+.tasks-move-row {
+ display: flex;
+ gap: var(--space-sm);
align-items: center;
- padding: 2px 6px;
- border-radius: 999px;
- font-size: 0.7rem;
- line-height: 1.2;
+}
+
+.tasks-select-inline {
+ min-width: 240px;
+}
+
+.tasks-comment-row {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.tasks-comments {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.tasks-comment {
+ padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
- color: var(--text-secondary);
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
}
-.tasks-label-editor {
+.tasks-comment-meta {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ margin-bottom: 6px;
+}
+
+.tasks-comment-text {
+ white-space: pre-wrap;
+ color: var(--text-primary);
+ line-height: 1.35;
+ font-size: 0.85rem;
+}
+
+.tasks-deps {
display: flex;
- flex-wrap: wrap;
+ flex-direction: column;
gap: 6px;
}
-.tasks-label-toggle {
- appearance: none;
- cursor: pointer;
+.tasks-dep-row {
+ display: grid;
+ grid-template-columns: 18px 1fr auto;
+ gap: 10px;
+ align-items: center;
+ padding: 8px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
}
-.tasks-label-toggle.is-selected {
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.25);
+.tasks-dep-row.done {
+ opacity: 0.75;
}
-.tasks-checkbox {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- font-size: 0.8rem;
- color: var(--text-secondary);
+.tasks-cover {
+ display: block;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ overflow: hidden;
}
-.tasks-checkbox input {
- margin: 0;
- accent-color: var(--accent-primary);
+.tasks-cover img {
+ display: block;
+ width: 100%;
+ max-height: 220px;
+ object-fit: cover;
}
-.tasks-label--green { border-color: #2ea043; background: rgba(46, 160, 67, 0.12); color: var(--text-primary); }
-.tasks-label--yellow { border-color: #d29922; background: rgba(210, 153, 34, 0.14); color: var(--text-primary); }
-.tasks-label--orange { border-color: #f78166; background: rgba(247, 129, 102, 0.14); color: var(--text-primary); }
-.tasks-label--red { border-color: #f85149; background: rgba(248, 81, 73, 0.14); color: var(--text-primary); }
-.tasks-label--purple { border-color: #a371f7; background: rgba(163, 113, 247, 0.14); color: var(--text-primary); }
-.tasks-label--blue { border-color: #1f6feb; background: rgba(31, 111, 235, 0.14); color: var(--text-primary); }
-.tasks-label--sky { border-color: #79c0ff; background: rgba(121, 192, 255, 0.14); color: var(--text-primary); }
-.tasks-label--lime { border-color: #7ee787; background: rgba(126, 231, 135, 0.14); color: var(--text-primary); }
-.tasks-label--pink { border-color: #ff80c8; background: rgba(255, 128, 200, 0.14); color: var(--text-primary); }
-.tasks-label--black { border-color: #30363d; background: rgba(48, 54, 61, 0.6); color: var(--text-primary); }
-.tasks-label--more { border-color: var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); }
+.tasks-cover.tasks-cover-color {
+ height: 110px;
+}
-.task-card-assignees {
- display: inline-flex;
+.tasks-attachments {
+ display: flex;
+ flex-direction: column;
gap: 6px;
- flex-shrink: 0;
}
-.tasks-avatar {
- width: 22px;
- height: 22px;
- border-radius: 999px;
+.tasks-attachment {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ padding: 8px 10px;
border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
+}
+
+.tasks-attachment-thumb {
display: inline-flex;
+ width: 44px;
+ height: 44px;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ border: 1px solid var(--border-color);
+ background: rgba(0, 0, 0, 0.08);
+ flex: 0 0 auto;
align-items: center;
justify-content: center;
- overflow: hidden;
- color: var(--text-secondary);
- font-size: 0.75rem;
- text-decoration: none;
}
-.tasks-avatar img {
+.tasks-attachment-thumb.is-empty {
+ opacity: 0.5;
+}
+
+.tasks-attachment-thumb img {
+ display: block;
width: 100%;
height: 100%;
object-fit: cover;
- display: block;
}
-.tasks-avatar-more {
- font-family: var(--font-mono);
- font-size: 0.7rem;
+.tasks-attachment-body {
+ min-width: 0;
+ flex: 1;
}
-.task-card-due {
- font-family: var(--font-mono);
- font-size: 0.72rem;
- color: var(--text-secondary);
+.tasks-attachment-name {
+ color: var(--text-primary);
+ font-size: 0.85rem;
+ line-height: 1.25;
+ text-decoration: none;
+ word-break: break-word;
+}
+
+.tasks-attachment-name:hover {
+ text-decoration: underline;
+}
+
+.tasks-attachment-meta {
+ margin-top: 2px;
+ color: var(--text-tertiary);
+ font-size: 0.75rem;
+ line-height: 1.2;
+ word-break: break-word;
}
-.tasks-kv {
+.tasks-checklists {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: var(--space-sm);
}
-.tasks-kv-row {
- display: grid;
- grid-template-columns: 140px 1fr;
- gap: 10px;
- padding: 8px 10px;
+.tasks-checklist {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--bg-secondary);
+ padding: 10px;
}
-.tasks-kv-row-edit {
+.tasks-checklist-header {
+ display: flex;
+ gap: 10px;
align-items: center;
+ justify-content: space-between;
}
-.tasks-kv-key {
- color: var(--text-secondary);
- font-size: 0.8rem;
+.tasks-checklist-title {
font-weight: 700;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.tasks-kv-val {
color: var(--text-primary);
- font-size: 0.85rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ font-size: 0.9rem;
+ min-width: 0;
+ word-break: break-word;
}
-.tasks-kv-val-edit {
- overflow: visible;
- text-overflow: unset;
- white-space: normal;
+.tasks-checklist-actions {
display: flex;
+ gap: 6px;
align-items: center;
- gap: 8px;
}
-.task-card-board {
- cursor: grab;
-}
-
-.task-card-board.dragging {
- opacity: 0.6;
- cursor: grabbing;
+.tasks-checkitems {
+ margin-top: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
-.task-card-row {
- padding: var(--space-sm) var(--space-md);
+.tasks-checkitem-row {
+ display: grid;
+ grid-template-columns: 18px 1fr auto;
+ gap: 10px;
+ align-items: center;
+ padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- background: var(--bg-tertiary);
+ background: var(--bg-primary);
+}
+
+.tasks-checkitem-row.done {
+ opacity: 0.75;
+}
+
+.tasks-checkitem-text {
+ min-width: 0;
+ color: var(--text-primary);
+ font-size: 0.85rem;
cursor: pointer;
- transition: border-color 0.15s, transform 0.15s;
+ word-break: break-word;
}
-.task-card-list {
+.tasks-checkitem-add,
+.tasks-checklist-add {
+ margin-top: 8px;
+}
+
+.tasks-list-manager-row {
display: flex;
- align-items: flex-start;
+ align-items: center;
justify-content: space-between;
gap: 10px;
+ padding: 10px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ margin-bottom: 8px;
}
-.task-card-list-main {
+.tasks-list-manager-name {
min-width: 0;
flex: 1;
+ color: var(--text-primary);
+ font-weight: 600;
+ word-break: break-word;
}
-.task-card-list-actions {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
+.tasks-list-manager-actions {
+ display: flex;
gap: 6px;
+ align-items: center;
}
-.task-card-row:hover {
- border-color: var(--accent-primary);
- transform: translateY(-1px);
+.tasks-dep-row.done .tasks-dep-text {
+ text-decoration: line-through;
}
-.task-card-row.active {
- border-color: var(--accent-primary);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
+.tasks-dep-text a {
+ color: var(--accent-primary);
+ text-decoration: none;
}
-.task-card-title {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 4px;
+.tasks-dep-text a:hover {
+ text-decoration: underline;
}
-.tasks-card-board-dot {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
- margin-right: 8px;
- transform: translateY(1px);
+.tasks-dep-remove {
+ padding: 4px 8px;
+ font-weight: 700;
}
-.task-card-meta {
- font-size: 0.75rem;
+.tasks-dep-add {
+ margin-top: var(--space-sm);
+}
+
+.tasks-detail-meta {
+ font-size: 0.8rem;
color: var(--text-tertiary);
+ margin-bottom: var(--space-md);
}
-.tasks-detail {
- grid-area: detail;
- grid-column: 2;
- grid-row: 1;
- overflow-y: auto;
- padding: var(--space-lg);
- background: var(--bg-primary);
- min-width: 0;
+.tasks-detail-desc {
+ white-space: pre-wrap;
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ padding: var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
}
-.tasks-detail-empty {
- color: var(--text-secondary);
+.tasks-config-hint {
+ padding: var(--space-lg);
+ border: 1px dashed var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary);
}
-.tasks-detail-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: var(--space-md);
+.tasks-config-title {
+ font-weight: 700;
margin-bottom: var(--space-sm);
}
-.tasks-detail-title {
- font-weight: 700;
- font-size: 1rem;
- line-height: 1.2;
+.tasks-config-text {
+ color: var(--text-secondary);
+ line-height: 1.4;
}
-.tasks-detail-actions {
- display: flex;
- gap: var(--space-sm);
- align-items: center;
- flex-wrap: wrap;
- justify-content: flex-end;
- flex-shrink: 0;
+/* Port panel - project detection styles */
+.port-name {
+ cursor: pointer;
+ transition: color 0.15s;
}
-.tasks-input {
- width: 100%;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- padding: 8px 10px;
- font-size: 0.95rem;
- font-weight: 700;
+.port-name:hover {
+ color: var(--accent-primary);
}
-.tasks-input-inline {
- width: auto;
- min-width: 220px;
- display: inline-flex;
- margin: 0 6px;
- font-weight: 600;
- font-size: 0.85rem;
- padding: 6px 10px;
+.port-name.custom-label {
+ color: #9f7aea;
}
-.tasks-textarea {
- width: 100%;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- padding: 8px 10px;
- font-size: 0.85rem;
- font-family: var(--font-mono);
- resize: vertical;
+.port-path {
+ display: block;
+ font-size: 0.7rem;
+ color: var(--accent-primary);
+ opacity: 0.8;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.tasks-inline-row {
+/* Port context - project/worktree info */
+.port-context {
display: flex;
- gap: var(--space-sm);
align-items: center;
- flex-wrap: wrap;
+ gap: 0.3rem;
+ font-size: 0.75rem;
+ margin-top: 2px;
+}
+
+.port-project {
+ color: #9f7aea;
+ font-weight: 500;
+}
+
+.port-worktree {
+ background: var(--accent-primary);
+ color: white;
+ padding: 1px 6px;
+ border-radius: 3px;
+ font-size: 0.7rem;
+ font-weight: 500;
+}
+
+.port-subpath {
+ color: var(--text-muted);
+ font-size: 0.7rem;
}
-.tasks-combined-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
+/* ============================================
+ Sidebar Ports Section
+ ============================================ */
+
+.sidebar-section {
+ border-top: 1px solid var(--border-color);
+ margin-top: auto;
}
-.tasks-combined-item {
+.sidebar-section-header {
display: flex;
align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ cursor: pointer;
+ user-select: none;
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ transition: background 0.15s;
}
-.tasks-combined-label {
- font-size: 0.85rem;
- color: var(--text-primary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.sidebar-section-header:hover {
+ background: var(--bg-tertiary);
}
-.tasks-combined-actions {
- display: inline-flex;
- gap: 6px;
- flex-shrink: 0;
+.ports-count {
+ background: var(--accent-primary);
+ color: white;
+ padding: 1px 6px;
+ border-radius: 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ min-width: 18px;
+ text-align: center;
}
-.tasks-chips {
- display: inline-flex;
- gap: 6px;
- flex-wrap: wrap;
- margin-left: 6px;
+.collapse-icon {
+ margin-left: auto;
+ font-size: 0.7rem;
+ transition: transform 0.2s;
}
-.tasks-chip {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 4px 8px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
- background: var(--bg-secondary);
- font-size: 0.75rem;
- color: var(--text-secondary);
+.sidebar-section.collapsed .collapse-icon {
+ transform: rotate(-90deg);
}
-.tasks-chip-avatar {
- width: 16px;
- height: 16px;
- border-radius: 999px;
- object-fit: cover;
- display: inline-block;
+.sidebar-section.collapsed .ports-sidebar-list {
+ display: none;
}
-.tasks-chip-link {
- color: var(--text-secondary);
- text-decoration: none;
+.ports-sidebar-list {
+ max-height: 200px;
+ overflow-y: auto;
+ padding: var(--space-xs) 0;
}
-.tasks-chip-link:hover {
- color: var(--text-primary);
- text-decoration: underline;
+.port-sidebar-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-xs) var(--space-md);
+ font-size: 0.75rem;
+ cursor: pointer;
+ transition: background 0.15s;
+ border-left: 2px solid transparent;
}
-.tasks-chip-muted {
- opacity: 0.7;
+.port-sidebar-item:hover {
+ background: var(--bg-tertiary);
}
-.tasks-chip-x {
- appearance: none;
- border: none;
- background: transparent;
- color: var(--text-tertiary);
- cursor: pointer;
+.port-sidebar-item.orchestrator { border-left-color: #9f7aea; }
+.port-sidebar-item.node { border-left-color: #68d391; }
+.port-sidebar-item.rails { border-left-color: #f56565; }
+.port-sidebar-item.python { border-left-color: #ffd93d; }
+.port-sidebar-item.vite { border-left-color: #48bb78; }
+
+.port-sidebar-icon {
font-size: 0.9rem;
- line-height: 1;
- padding: 0 2px;
+ flex-shrink: 0;
}
-.tasks-chip-x:hover {
+.port-sidebar-info {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.port-sidebar-name {
+ display: block;
+ font-weight: 500;
color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-detail-block {
- margin-top: var(--space-md);
+.port-sidebar-context {
+ display: block;
+ font-size: 0.65rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-detail-block.tasks-dropzone-hover {
- outline: 2px dashed var(--accent-color);
- outline-offset: 6px;
- border-radius: var(--radius-md);
+.port-sidebar-port {
+ color: var(--accent-primary);
+ font-weight: 500;
+ font-size: 0.7rem;
+ flex-shrink: 0;
}
-.tasks-detail-block-title {
- font-size: 0.8rem;
- color: var(--text-secondary);
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- margin-bottom: 6px;
+.ports-sidebar-empty {
+ padding: var(--space-sm) var(--space-md);
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-style: italic;
}
-.tasks-move-row {
- display: flex;
- gap: var(--space-sm);
- align-items: center;
+/* ============================================
+ Dashboard Ports Section
+ ============================================ */
+
+.ports-dashboard-section {
+ margin-top: var(--space-lg);
}
-.tasks-select-inline {
- min-width: 240px;
+.ports-dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: var(--space-md);
}
-.tasks-comment-row {
+.port-dashboard-card {
display: flex;
- flex-direction: column;
+ align-items: center;
gap: var(--space-sm);
+ padding: var(--space-md);
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ border-left: 3px solid var(--border-color);
+ cursor: pointer;
+ transition: all 0.15s;
}
-.tasks-comments {
- display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+.port-dashboard-card:hover {
+ background: var(--bg-tertiary);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-soft);
}
-.tasks-comment {
- padding: var(--space-sm) var(--space-md);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+.port-dashboard-card.orchestrator { border-left-color: #9f7aea; }
+.port-dashboard-card.node { border-left-color: #68d391; }
+.port-dashboard-card.rails { border-left-color: #f56565; }
+.port-dashboard-card.ruby { border-left-color: #f56565; }
+.port-dashboard-card.python { border-left-color: #ffd93d; }
+.port-dashboard-card.vite { border-left-color: #48bb78; }
+
+.port-card-icon {
+ font-size: 1.5rem;
+ flex-shrink: 0;
}
-.tasks-comment-meta {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-bottom: 6px;
+.port-card-info {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
}
-.tasks-comment-text {
- white-space: pre-wrap;
+.port-card-name {
+ display: block;
+ font-weight: 600;
color: var(--text-primary);
- line-height: 1.35;
- font-size: 0.85rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-deps {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.port-card-context {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-dep-row {
- display: grid;
- grid-template-columns: 18px 1fr auto;
- gap: 10px;
- align-items: center;
- padding: 8px 10px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+.port-card-port {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--accent-primary);
+ flex-shrink: 0;
}
-.tasks-dep-row.done {
- opacity: 0.75;
+.ports-empty,
+.ports-loading {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: var(--space-lg);
+ color: var(--text-muted);
+ font-style: italic;
}
-.tasks-cover {
- display: block;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- overflow: hidden;
-}
+/* ============================================
+ Dashboard Split Row Layout
+ ============================================ */
-.tasks-cover img {
- display: block;
- width: 100%;
- max-height: 220px;
- object-fit: cover;
+.dashboard-split-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-lg);
+ margin-top: var(--space-lg);
}
-.tasks-cover.tasks-cover-color {
- height: 110px;
+.dashboard-half {
+ background: var(--bg-secondary);
+ border-radius: var(--radius-lg);
+ padding: var(--space-md);
+ max-height: 400px;
+ overflow-y: auto;
}
-.tasks-attachments {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.dashboard-half h2 {
+ margin: 0 0 var(--space-sm) 0;
+ font-size: 1rem;
+ position: sticky;
+ top: 0;
+ background: var(--bg-secondary);
+ padding-bottom: var(--space-xs);
}
-.tasks-attachment {
- display: flex;
- gap: 10px;
- align-items: center;
- padding: 8px 10px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+/* Grid layout for ports in dashboard half */
+.dashboard-half .ports-dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: var(--space-xs);
}
-.tasks-attachment-thumb {
- display: inline-flex;
- width: 44px;
- height: 44px;
- border-radius: var(--radius-sm);
- overflow: hidden;
- border: 1px solid var(--border-color);
- background: rgba(0, 0, 0, 0.08);
- flex: 0 0 auto;
+.dashboard-half .port-dashboard-card {
+ padding: var(--space-sm);
+ flex-direction: column;
align-items: center;
- justify-content: center;
+ text-align: center;
+ gap: var(--space-xs);
+ min-height: 70px;
}
-.tasks-attachment-thumb.is-empty {
- opacity: 0.5;
+.dashboard-half .port-card-icon {
+ font-size: 1.3rem;
}
-.tasks-attachment-thumb img {
- display: block;
+.dashboard-half .port-card-info {
width: 100%;
- height: 100%;
- object-fit: cover;
}
-.tasks-attachment-body {
- min-width: 0;
- flex: 1;
+.dashboard-half .port-card-name {
+ font-size: 0.75rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-attachment-name {
- color: var(--text-primary);
- font-size: 0.85rem;
- line-height: 1.25;
- text-decoration: none;
- word-break: break-word;
+.dashboard-half .port-card-context {
+ font-size: 0.65rem;
}
-.tasks-attachment-name:hover {
- text-decoration: underline;
+.dashboard-half .port-card-port {
+ font-size: 0.8rem;
+ margin-top: auto;
}
-.tasks-attachment-meta {
- margin-top: 2px;
- color: var(--text-tertiary);
- font-size: 0.75rem;
- line-height: 1.2;
- word-break: break-word;
+/* Grid layout for quick links in dashboard half */
+.dashboard-half .quick-links-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: var(--space-xs);
}
-.tasks-checklists {
- display: flex;
+.dashboard-half .quick-link-item {
flex-direction: column;
- gap: var(--space-sm);
+ align-items: center;
+ text-align: center;
+ padding: var(--space-sm);
+ min-height: 60px;
+ gap: var(--space-xs);
+ border-left: none;
+ border-bottom: 2px solid var(--accent-primary);
}
-.tasks-checklist {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- padding: 10px;
+.dashboard-half .quick-link-item .quick-link-icon {
+ font-size: 1.2rem;
}
-.tasks-checklist-header {
- display: flex;
- gap: 10px;
- align-items: center;
- justify-content: space-between;
+.dashboard-half .quick-link-item .quick-link-label {
+ font-size: 0.7rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
}
-.tasks-checklist-title {
- font-weight: 700;
- color: var(--text-primary);
- font-size: 0.9rem;
- min-width: 0;
- word-break: break-word;
+/* Quick links container inside dashboard */
+.dashboard-half .quick-links-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: var(--space-xs);
}
-.tasks-checklist-actions {
- display: flex;
- gap: 6px;
- align-items: center;
+.dashboard-half .quick-links-section {
+ display: contents;
}
-.tasks-checkitems {
- margin-top: 8px;
- display: flex;
- flex-direction: column;
- gap: 6px;
+.dashboard-half .quick-links-section h3 {
+ display: none;
}
-.tasks-checkitem-row {
- display: grid;
- grid-template-columns: 18px 1fr auto;
- gap: 10px;
- align-items: center;
- padding: 8px 10px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-primary);
+@media (max-width: 900px) {
+ .dashboard-split-row {
+ grid-template-columns: 1fr;
+ }
}
-.tasks-checkitem-row.done {
- opacity: 0.75;
-}
+/* ============================================
+ Quick Link Items (for dashboard half)
+ ============================================ */
-.tasks-checkitem-text {
- min-width: 0;
- color: var(--text-primary);
- font-size: 0.85rem;
+.quick-link-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--accent-primary);
cursor: pointer;
- word-break: break-word;
+ transition: all 0.15s;
+ text-decoration: none;
+ color: inherit;
+ border: none;
+ width: 100%;
+ text-align: left;
+ font-size: inherit;
+ font-family: inherit;
}
-.tasks-checkitem-add,
-.tasks-checklist-add {
- margin-top: 8px;
+.quick-link-item:hover {
+ background: var(--bg-primary);
+ transform: translateX(2px);
}
-.tasks-list-manager-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- margin-bottom: 8px;
+.quick-link-icon {
+ font-size: 1rem;
+ flex-shrink: 0;
}
-.tasks-list-manager-name {
- min-width: 0;
+.quick-link-label {
flex: 1;
+ font-weight: 500;
color: var(--text-primary);
- font-weight: 600;
- word-break: break-word;
}
-.tasks-list-manager-actions {
- display: flex;
- gap: 6px;
- align-items: center;
-}
-
-.tasks-dep-row.done .tasks-dep-text {
- text-decoration: line-through;
+.quick-links-empty {
+ padding: var(--space-md);
+ text-align: center;
+ color: var(--text-muted);
+ font-style: italic;
+ grid-column: 1 / -1;
}
-.tasks-dep-text a {
- color: var(--accent-primary);
- text-decoration: none;
-}
+/* ============================================
+ Session Recovery Dialog
+ ============================================ */
-.tasks-dep-text a:hover {
- text-decoration: underline;
+.recovery-modal .modal-content {
+ max-width: 600px;
+ width: 90vw;
}
-.tasks-dep-remove {
- padding: 4px 8px;
- font-weight: 700;
+.recovery-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
}
-.tasks-dep-add {
- margin-top: var(--space-sm);
+.recovery-header h2 {
+ margin: 0;
+ font-size: 1.1rem;
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
}
-.tasks-detail-meta {
+.recovery-info {
+ padding: var(--space-sm) var(--space-lg);
+ background: var(--bg-secondary);
font-size: 0.8rem;
- color: var(--text-tertiary);
- margin-bottom: var(--space-md);
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border-color);
}
-.tasks-detail-desc {
- white-space: pre-wrap;
- font-family: var(--font-mono);
- font-size: 0.85rem;
+.recovery-sessions {
+ max-height: 300px;
+ overflow-y: auto;
padding: var(--space-md);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- color: var(--text-primary);
}
-.tasks-config-hint {
- padding: var(--space-lg);
- border: 1px dashed var(--border-color);
- border-radius: var(--radius-md);
+.recovery-session {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-md);
+ padding: var(--space-sm) var(--space-md);
+ margin-bottom: var(--space-xs);
background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--accent-primary);
}
-.tasks-config-title {
- font-weight: 700;
- margin-bottom: var(--space-sm);
-}
-
-.tasks-config-text {
- color: var(--text-secondary);
- line-height: 1.4;
-}
-
-/* Port panel - project detection styles */
-.port-name {
- cursor: pointer;
- transition: color 0.15s;
+.recovery-session.selected {
+ border-left-color: #48bb78;
+ background: rgba(72, 187, 120, 0.1);
}
-.port-name:hover {
- color: var(--accent-primary);
+.recovery-checkbox {
+ margin-top: 2px;
}
-.port-name.custom-label {
- color: #9f7aea;
+.recovery-session-info {
+ flex: 1;
+ min-width: 0;
}
-.port-path {
- display: block;
- font-size: 0.7rem;
- color: var(--accent-primary);
- opacity: 0.8;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.recovery-session-id {
+ font-weight: 600;
+ font-size: 0.9rem;
+ color: var(--text-primary);
}
-/* Port context - project/worktree info */
-.port-context {
- display: flex;
- align-items: center;
- gap: 0.3rem;
+.recovery-session-details {
font-size: 0.75rem;
+ color: var(--text-muted);
margin-top: 2px;
}
-.port-project {
- color: #9f7aea;
- font-weight: 500;
+.recovery-session-details span {
+ display: inline-block;
+ margin-right: var(--space-sm);
}
-.port-worktree {
- background: var(--accent-primary);
- color: white;
- padding: 1px 6px;
- border-radius: 3px;
- font-size: 0.7rem;
- font-weight: 500;
+.recovery-session-cwd {
+ color: var(--accent-primary);
}
-.port-subpath {
- color: var(--text-muted);
- font-size: 0.7rem;
+.recovery-session-agent {
+ background: var(--bg-primary);
+ padding: 1px 6px;
+ border-radius: 3px;
}
-/* ============================================
- Sidebar Ports Section
- ============================================ */
-
-.sidebar-section {
+.recovery-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border-color);
- margin-top: auto;
+ gap: var(--space-md);
}
-.sidebar-section-header {
+.recovery-actions {
display: flex;
- align-items: center;
gap: var(--space-sm);
+}
+
+.recovery-skip {
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+
+.btn-recovery {
padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-sm);
+ border: none;
cursor: pointer;
- user-select: none;
- font-size: 0.85rem;
font-weight: 500;
- color: var(--text-secondary);
- transition: background 0.15s;
+ transition: all 0.15s;
}
-.sidebar-section-header:hover {
- background: var(--bg-tertiary);
+.btn-recovery-all {
+ background: #48bb78;
+ color: white;
}
-.ports-count {
+.btn-recovery-all:hover {
+ background: #38a169;
+}
+
+.btn-recovery-selected {
background: var(--accent-primary);
- color: white;
- padding: 1px 6px;
- border-radius: 10px;
- font-size: 0.7rem;
- font-weight: 600;
- min-width: 18px;
- text-align: center;
+ color: var(--text-on-accent);
}
-.collapse-icon {
- margin-left: auto;
- font-size: 0.7rem;
- transition: transform 0.2s;
+.btn-recovery-selected:hover {
+ background: var(--accent-primary-hover);
}
-.sidebar-section.collapsed .collapse-icon {
- transform: rotate(-90deg);
+.btn-recovery-clear {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
}
-.sidebar-section.collapsed .ports-sidebar-list {
- display: none;
+.btn-recovery-clear:hover {
+ border-color: var(--accent-danger);
+ background: rgba(248, 81, 73, 0.14);
}
-.ports-sidebar-list {
- max-height: 200px;
- overflow-y: auto;
- padding: var(--space-xs) 0;
+.btn-recovery-skip {
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.port-sidebar-item {
- display: flex;
- align-items: center;
- gap: var(--space-xs);
- padding: var(--space-xs) var(--space-md);
- font-size: 0.75rem;
- cursor: pointer;
- transition: background 0.15s;
- border-left: 2px solid transparent;
+.btn-recovery-skip:hover {
+ background: var(--bg-primary);
}
-.port-sidebar-item:hover {
- background: var(--bg-tertiary);
+.no-recovery {
+ text-align: center;
+ padding: var(--space-lg);
+ color: var(--text-muted);
}
-.port-sidebar-item.orchestrator { border-left-color: #9f7aea; }
-.port-sidebar-item.node { border-left-color: #68d391; }
-.port-sidebar-item.rails { border-left-color: #f56565; }
-.port-sidebar-item.python { border-left-color: #ffd93d; }
-.port-sidebar-item.vite { border-left-color: #48bb78; }
+/* ============================================
+ Activity Feed Styles
+ ============================================ */
-.port-sidebar-icon {
- font-size: 0.9rem;
- flex-shrink: 0;
+.activity-feed-modal .activity-feed-content {
+ max-width: 1200px;
+ width: 94vw;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
}
-.port-sidebar-info {
+.activity-list {
flex: 1;
- min-width: 0;
- overflow: hidden;
+ overflow-y: auto;
+ padding: var(--space-md) var(--space-lg);
}
-.port-sidebar-name {
- display: block;
- font-weight: 500;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-empty {
+ padding: var(--space-lg);
+ color: var(--text-muted);
}
-.port-sidebar-context {
- display: block;
- font-size: 0.65rem;
- color: var(--text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-event {
+ border: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ padding: var(--space-md);
+ margin-bottom: var(--space-sm);
+}
+
+.activity-event.activity-failed {
+ border-color: rgba(248, 81, 73, 0.55);
+ box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.18);
+}
+
+.activity-meta {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: center;
+ justify-content: space-between;
}
-.port-sidebar-port {
- color: var(--accent-primary);
- font-weight: 500;
- font-size: 0.7rem;
- flex-shrink: 0;
+.activity-actions {
+ display: flex;
+ gap: var(--space-xs);
+ align-items: center;
+ margin-left: auto;
}
-.ports-sidebar-empty {
- padding: var(--space-sm) var(--space-md);
+.activity-action-btn {
+ padding: 4px 8px;
font-size: 0.75rem;
- color: var(--text-muted);
- font-style: italic;
+ line-height: 1;
}
-/* ============================================
- Dashboard Ports Section
- ============================================ */
-
-.ports-dashboard-section {
- margin-top: var(--space-lg);
+.activity-time {
+ color: var(--text-muted);
+ font-size: 0.8rem;
}
-.ports-dashboard-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: var(--space-md);
+.activity-kind {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ padding: 2px 8px;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.port-dashboard-card {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
- padding: var(--space-md);
- background: var(--bg-secondary);
- border-radius: var(--radius-md);
- border-left: 3px solid var(--border-color);
- cursor: pointer;
- transition: all 0.15s;
+.activity-kind-agent {
+ border-color: var(--accent-primary);
+ color: var(--accent-primary);
+ background: rgba(31, 111, 235, 0.12);
}
-.port-dashboard-card:hover {
- background: var(--bg-tertiary);
- transform: translateY(-2px);
- box-shadow: var(--shadow-soft);
+.activity-kind-session {
+ border-color: var(--accent-success);
+ color: var(--accent-success);
+ background: rgba(63, 185, 80, 0.12);
}
-.port-dashboard-card.orchestrator { border-left-color: #9f7aea; }
-.port-dashboard-card.node { border-left-color: #68d391; }
-.port-dashboard-card.rails { border-left-color: #f56565; }
-.port-dashboard-card.ruby { border-left-color: #f56565; }
-.port-dashboard-card.python { border-left-color: #ffd93d; }
-.port-dashboard-card.vite { border-left-color: #48bb78; }
+.activity-kind-server {
+ border-color: var(--accent-warning);
+ color: var(--accent-warning);
+ background: rgba(210, 153, 34, 0.12);
+}
-.port-card-icon {
- font-size: 1.5rem;
- flex-shrink: 0;
+.activity-kind-git {
+ border-color: var(--accent-success);
+ color: var(--accent-success);
+ background: rgba(63, 185, 80, 0.12);
}
-.port-card-info {
- flex: 1;
- min-width: 0;
- overflow: hidden;
+.activity-kind-pr {
+ border-color: var(--accent-primary);
+ color: var(--accent-primary);
+ background: rgba(31, 111, 235, 0.12);
}
-.port-card-name {
- display: block;
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-kind-tests {
+ border-color: var(--accent-warning);
+ color: var(--accent-warning);
+ background: rgba(210, 153, 34, 0.12);
}
-.port-card-context {
- display: block;
- font-size: 0.75rem;
- color: var(--text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-kind-build {
+ border-color: var(--accent-warning);
+ color: var(--accent-warning);
+ background: rgba(210, 153, 34, 0.12);
}
-.port-card-port {
- font-size: 1rem;
- font-weight: 700;
- color: var(--accent-primary);
- flex-shrink: 0;
+.activity-summary {
+ margin-top: var(--space-sm);
+ color: var(--text-primary);
+ font-size: 0.95rem;
}
-.ports-empty,
-.ports-loading {
- grid-column: 1 / -1;
- text-align: center;
- padding: var(--space-lg);
+.activity-data {
+ margin-top: var(--space-sm);
color: var(--text-muted);
- font-style: italic;
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ white-space: pre-wrap;
+ word-break: break-word;
}
-/* ============================================
- Dashboard Split Row Layout
- ============================================ */
+/* ==========================================================
+ ONBOARDING OVERLAY (PERF-OPTIMIZED)
+ ========================================================== */
-.dashboard-split-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: var(--space-lg);
- margin-top: var(--space-lg);
+.onboarding-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+ background:
+ radial-gradient(90% 60% at 8% 12%, rgba(31, 111, 235, 0.22), transparent 75%),
+ radial-gradient(78% 55% at 92% 88%, rgba(0, 195, 255, 0.14), transparent 80%),
+ rgba(5, 8, 15, 0.86);
+ overflow: auto;
+ transition: opacity 0.18s ease-out, visibility 0.18s linear;
}
.dashboard-half {
@@ -11669,13 +12895,18 @@ header h1 {
overflow-y: auto;
}
-.dashboard-half h2 {
- margin: 0 0 var(--space-sm) 0;
- font-size: 1rem;
- position: sticky;
- top: 0;
- background: var(--bg-secondary);
- padding-bottom: var(--space-xs);
+.onboarding-bg-glow {
+ position: absolute;
+ width: 52vw;
+ height: 52vw;
+ max-width: 640px;
+ max-height: 640px;
+ border-radius: 50%;
+ top: -16%;
+ left: -6%;
+ background: radial-gradient(circle, rgba(31, 111, 235, 0.2) 0%, rgba(31, 111, 235, 0) 72%);
+ opacity: 0.75;
+ pointer-events: none;
}
/* Grid layout for ports in dashboard half */
@@ -11698,8 +12929,9 @@ header h1 {
font-size: 1.1rem;
}
-.dashboard-half .port-card-info {
- width: 100%;
+.onboarding-header {
+ margin-bottom: 4px;
+ text-align: center;
}
.dashboard-half .port-card-name {
@@ -11725,9 +12957,14 @@ header h1 {
gap: var(--space-xs);
}
-.dashboard-half .quick-link-item {
- flex-direction: column;
- align-items: center;
+.onboarding-welcome-card {
+ padding: 28px;
+ border-radius: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ background:
+ linear-gradient(155deg, rgba(56, 139, 253, 0.13), rgba(20, 29, 44, 0.42)),
+ rgba(255, 255, 255, 0.04);
+ box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28);
text-align: center;
padding: 6px;
min-height: 52px;
@@ -11755,99 +12992,100 @@ header h1 {
gap: var(--space-xs);
}
-.dashboard-half .quick-links-section {
- display: contents;
+.onboarding-welcome-notes {
+ margin-top: 14px;
+ display: grid;
+ gap: 6px;
+ text-align: center;
}
-.dashboard-half .quick-links-section h3 {
- display: none;
+.onboarding-welcome-notes p {
+ margin: 0;
+ color: rgba(255, 255, 255, 0.74);
+ font-size: 0.9rem;
+ line-height: 1.45;
}
-@media (max-width: 900px) {
- .dashboard-split-row {
- grid-template-columns: 1fr;
- }
+.onboarding-welcome-actions {
+ margin-top: 46px;
+ justify-content: center;
}
-/* ============================================
- Quick Link Items (for dashboard half)
- ============================================ */
-
-.quick-link-item {
+.onboarding-stepper-row {
display: flex;
align-items: center;
- gap: var(--space-sm);
- padding: var(--space-sm) var(--space-md);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- border-left: 3px solid var(--accent-primary);
- cursor: pointer;
- transition: all 0.15s;
- text-decoration: none;
- color: inherit;
- border: none;
+ justify-content: center;
width: 100%;
- text-align: left;
- font-size: inherit;
- font-family: inherit;
+ gap: 14px;
+ margin-bottom: 18px;
+ flex-wrap: wrap;
+ row-gap: 12px;
}
-.quick-link-item:hover {
- background: var(--bg-primary);
- transform: translateX(2px);
+.onboarding-stepper-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-.quick-link-icon {
- font-size: 1rem;
- flex-shrink: 0;
+.stepper-icon-box {
+ display: flex;
+ align-items: center;
+ position: relative;
}
-.quick-link-label {
- flex: 1;
- font-weight: 500;
- color: var(--text-primary);
+.stepper-diamond {
+ width: 14px;
+ height: 14px;
+ border-radius: 3px;
+ transform: rotate(45deg);
+ transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}
-.quick-links-empty {
- padding: var(--space-md);
- text-align: center;
- color: var(--text-muted);
- font-style: italic;
- grid-column: 1 / -1;
+.stepper-upcoming .stepper-diamond {
+ background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+ box-shadow: inset 0 0 0 1px rgba(229, 231, 235, 0.26);
+ opacity: 0.95;
}
-/* ============================================
- Session Recovery Dialog
- ============================================ */
-
-.recovery-modal .modal-content {
- max-width: 600px;
- width: 90vw;
+.stepper-active .stepper-icon-box {
+ gap: 0;
}
-.recovery-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-tertiary);
+.stepper-active .stepper-diamond {
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+ box-shadow: 0 0 10px rgba(59, 130, 246, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.34);
+ transform: rotate(45deg) scale(1.15);
+ opacity: 1;
}
-.recovery-header h2 {
- margin: 0;
- font-size: 1.1rem;
- display: flex;
- align-items: center;
- gap: var(--space-sm);
+.stepper-done .stepper-diamond {
+ background: linear-gradient(135deg, #34d399 0%, #16a34a 100%);
+ box-shadow: 0 0 10px rgba(34, 197, 94, 0.38), inset 0 0 0 1px rgba(236, 253, 245, 0.34);
+ opacity: 1;
}
-.recovery-info {
- padding: var(--space-sm) var(--space-lg);
- background: var(--bg-secondary);
- font-size: 0.8rem;
- color: var(--text-muted);
- border-bottom: 1px solid var(--border-color);
+.stepper-active-label {
+ position: absolute;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 0.88rem;
+ color: rgba(255, 255, 255, 0.9);
+ font-weight: 520;
+ margin-left: 0;
+ white-space: nowrap;
+}
+
+.onboarding-step-card {
+ position: relative;
+ overflow: hidden;
+ padding: 26px;
+ border-radius: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.045);
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
+ transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.recovery-metrics {
@@ -11893,254 +13131,318 @@ header h1 {
padding: var(--space-md);
}
-.recovery-session {
+.onboarding-step-icon {
+ width: 42px;
+ height: 42px;
+ margin-bottom: 18px;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.11);
+ color: rgba(255, 255, 255, 0.94);
display: flex;
- align-items: flex-start;
- gap: var(--space-md);
- padding: var(--space-sm) var(--space-md);
- margin-bottom: var(--space-xs);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- border-left: 3px solid var(--accent-primary);
+ align-items: center;
+ justify-content: center;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
-.recovery-session.selected {
- border-left-color: #48bb78;
- background: rgba(72, 187, 120, 0.1);
+.onboarding-step-icon-svg {
+ width: 22px;
+ height: 22px;
+ display: block;
}
-.recovery-checkbox {
- margin-top: 2px;
+.onboarding-step-icon-svg * {
+ vector-effect: non-scaling-stroke;
}
-.recovery-session-info {
- flex: 1;
- min-width: 0;
+.onboarding-step-title {
+ font-size: clamp(1.42rem, 2.9vw, 1.78rem);
+ font-weight: 620;
+ color: #ffffff;
+ margin-bottom: 10px;
}
-.recovery-session-id {
- font-weight: 600;
- font-size: 0.9rem;
- color: var(--text-primary);
+.onboarding-step-status-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ margin-bottom: 20px;
}
-.recovery-session-details {
- font-size: 0.75rem;
- color: var(--text-muted);
- margin-top: 2px;
+.onboarding-check {
+ margin-top: 3px;
+ flex-shrink: 0;
+ color: #3fb950;
}
-.recovery-session-details span {
- display: inline-block;
- margin-right: var(--space-sm);
+.onboarding-step-desc {
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.48;
+ color: rgba(255, 255, 255, 0.78);
}
-.recovery-session-cwd {
- color: var(--accent-primary);
+.onboarding-inline-status {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.9em;
+ background: rgba(0, 0, 0, 0.3);
}
-.recovery-session-agent {
- background: var(--bg-primary);
- padding: 1px 6px;
- border-radius: 3px;
-}
+.onboarding-inline-status.status-ok { color: #3fb950; }
+.onboarding-inline-status.status-missing { color: #f85149; }
+.onboarding-inline-status.status-pending { color: #58a6ff; }
-.recovery-footer {
+.onboarding-step-actions {
display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-top: 1px solid var(--border-color);
- gap: var(--space-md);
+ gap: 10px;
+ flex-wrap: wrap;
}
-.recovery-actions {
- display: flex;
- gap: var(--space-sm);
+.onboarding-btn-secondary,
+.onboarding-btn-back,
+.onboarding-btn-primary {
+ border-radius: 10px;
+ cursor: pointer;
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
-.recovery-skip {
- color: var(--text-muted);
- font-size: 0.8rem;
+.onboarding-btn-secondary {
+ padding: 10px 18px;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.09);
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 0.94rem;
+ font-weight: 520;
}
-.btn-recovery {
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-sm);
- border: none;
- cursor: pointer;
- font-weight: 500;
- transition: all 0.15s;
+.onboarding-btn-secondary:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.16);
+ border-color: rgba(255, 255, 255, 0.26);
+ transform: translateY(-1px);
}
-.btn-recovery-all {
- background: #48bb78;
- color: white;
+.onboarding-btn-secondary:disabled {
+ opacity: 0.52;
+ cursor: not-allowed;
}
-.btn-recovery-all:hover {
- background: #38a169;
+.onboarding-step-actions [data-setup-run] {
+ border-color: rgba(74, 222, 128, 0.55);
+ background: linear-gradient(135deg, #34d399 0%, #16a34a 100%);
+ color: #052e16;
+ box-shadow: 0 6px 16px rgba(22, 163, 74, 0.3);
}
-.btn-recovery-selected {
- background: var(--accent-primary);
- color: var(--text-on-accent);
+.onboarding-step-actions [data-setup-run]:hover:not(:disabled) {
+ border-color: rgba(134, 239, 172, 0.92);
+ background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
+ color: #052e16;
+ box-shadow: 0 8px 18px rgba(22, 163, 74, 0.38);
}
-.btn-recovery-selected:hover {
- background: var(--accent-primary-hover);
+.onboarding-step-actions [data-setup-run]:disabled {
+ border-color: rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.55);
+ box-shadow: none;
}
-.btn-recovery-clear {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
+.onboarding-nav-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 6px;
}
-.btn-recovery-clear:hover {
- border-color: var(--accent-danger);
- background: rgba(248, 81, 73, 0.14);
+.onboarding-nav-row.onboarding-welcome-actions {
+ justify-content: center;
}
-.btn-recovery-skip {
- background: var(--bg-tertiary);
- color: var(--text-secondary);
+.onboarding-btn-back {
+ padding: 13px 24px;
+ border: 1px solid rgba(255, 255, 255, 0.11);
+ background: rgba(255, 255, 255, 0.055);
+ color: rgba(255, 255, 255, 0.74);
+ font-size: 0.98rem;
+ font-weight: 600;
}
-.btn-recovery-skip:hover {
- background: var(--bg-primary);
+.onboarding-btn-back:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.9);
}
-.no-recovery {
- text-align: center;
- padding: var(--space-lg);
- color: var(--text-muted);
+.onboarding-btn-primary {
+ padding: 13px 28px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ background: linear-gradient(135deg, #388bfd 0%, #1f6feb 100%);
+ color: #ffffff;
+ font-size: 0.98rem;
+ font-weight: 620;
+ box-shadow: 0 4px 13px rgba(31, 111, 235, 0.34);
}
-/* ============================================
- Activity Feed Styles
- ============================================ */
-
-.activity-feed-modal .activity-feed-content {
- max-width: 1200px;
- width: 94vw;
- max-height: 90vh;
- display: flex;
- flex-direction: column;
+.onboarding-btn-primary:hover:not(:disabled) {
+ background: linear-gradient(135deg, #58a6ff 0%, #388bfd 100%);
+ transform: translateY(-1px);
+ box-shadow: 0 6px 16px rgba(31, 111, 235, 0.44);
}
-.activity-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-md) var(--space-lg);
+.onboarding-btn-primary:disabled {
+ border-color: transparent;
+ background: rgba(255, 255, 255, 0.11);
+ color: rgba(255, 255, 255, 0.45);
+ box-shadow: none;
+ cursor: not-allowed;
}
-.activity-empty {
- padding: var(--space-lg);
- color: var(--text-muted);
+.dependency-git-identity-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-bottom: 18px;
}
-.activity-event {
- border: 1px solid var(--border-color);
- background: var(--bg-secondary);
- border-radius: var(--radius-md);
- padding: var(--space-md);
- margin-bottom: var(--space-sm);
+.dependency-git-identity-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
}
-.activity-event.activity-failed {
- border-color: rgba(248, 81, 73, 0.55);
- box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.18);
+.dependency-git-identity-field span {
+ font-size: 0.83rem;
+ color: rgba(255, 255, 255, 0.66);
}
-.activity-meta {
- display: flex;
- gap: var(--space-sm);
- align-items: center;
- justify-content: space-between;
+.dependency-git-identity-field input {
+ padding: 10px 13px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ background: rgba(0, 0, 0, 0.28);
+ color: #fff;
+ font-family: var(--font-sans);
+ font-size: 0.98rem;
+ transition: border-color 0.14s ease, background-color 0.14s ease, box-shadow 0.14s ease;
}
-.activity-actions {
- display: flex;
- gap: var(--space-xs);
- align-items: center;
- margin-left: auto;
+.dependency-git-identity-field input:focus {
+ outline: none;
+ border-color: #388bfd;
+ background: rgba(0, 0, 0, 0.38);
+ box-shadow: 0 0 0 2px rgba(56, 139, 253, 0.2);
}
-.activity-action-btn {
- padding: 4px 8px;
- font-size: 0.75rem;
- line-height: 1;
+.dependency-gh-login-helper-text {
+ margin-bottom: 10px;
+ font-size: 0.93rem;
+ color: rgba(255, 255, 255, 0.82);
}
-.activity-time {
- color: var(--text-muted);
- font-size: 0.8rem;
+.dependency-onboarding-command-wrap {
+ margin: 18px 0;
+ padding: 14px;
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.09);
+ background: rgba(0, 0, 0, 0.32);
}
-.activity-kind {
+.dependency-onboarding-command-wrap pre {
+ margin: 0;
+ color: #a5d6ff;
font-family: var(--font-mono);
- font-size: 0.8rem;
- padding: 2px 8px;
- border-radius: 999px;
- border: 1px solid transparent;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
+ font-size: 0.83rem;
+ white-space: pre-wrap;
}
-.activity-kind-agent {
- border-color: var(--accent-primary);
- color: var(--accent-primary);
- background: rgba(31, 111, 235, 0.12);
+.dependency-gh-login-code-wrap {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
}
-.activity-kind-session {
- border-color: var(--accent-success);
- color: var(--accent-success);
- background: rgba(63, 185, 80, 0.12);
+.dependency-gh-login-helper-actions {
+ margin-top: 10px;
}
-.activity-kind-server {
- border-color: var(--accent-warning);
- color: var(--accent-warning);
- background: rgba(210, 153, 34, 0.12);
+.dependency-gh-login-code {
+ padding: 8px 14px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(0, 0, 0, 0.5);
+ font-size: 1.1rem;
+ letter-spacing: 2px;
+ color: #fff;
}
-.activity-kind-git {
- border-color: var(--accent-success);
- color: var(--accent-success);
- background: rgba(63, 185, 80, 0.12);
+.onboarding-close-btn {
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ z-index: 100;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.78);
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
-.activity-kind-pr {
- border-color: var(--accent-primary);
- color: var(--accent-primary);
- background: rgba(31, 111, 235, 0.12);
+.onboarding-close-btn:hover {
+ background: rgba(255, 255, 255, 0.18);
+ border-color: rgba(255, 255, 255, 0.22);
+ color: #fff;
}
-.activity-kind-tests {
- border-color: var(--accent-warning);
- color: var(--accent-warning);
- background: rgba(210, 153, 34, 0.12);
+.onboarding-close-btn.hidden {
+ display: none;
}
-.activity-kind-build {
- border-color: var(--accent-warning);
- color: var(--accent-warning);
- background: rgba(210, 153, 34, 0.12);
-}
+@media (max-width: 800px) {
+ .onboarding-overlay {
+ padding: 12px;
+ }
-.activity-summary {
- margin-top: var(--space-sm);
- color: var(--text-primary);
- font-size: 0.95rem;
+ .onboarding-container {
+ padding: 20px;
+ max-height: calc(100vh - 24px);
+ }
+
+ .onboarding-step-card {
+ padding: 20px;
+ }
+
+ .onboarding-nav-row {
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
+ .onboarding-btn-back,
+ .onboarding-btn-primary {
+ flex: 1 1 100%;
+ justify-content: center;
+ }
}
-.activity-data {
- margin-top: var(--space-sm);
- color: var(--text-muted);
- font-family: var(--font-mono);
- font-size: 0.8rem;
- white-space: pre-wrap;
- word-break: break-word;
+@media (prefers-reduced-motion: reduce) {
+ .onboarding-overlay,
+ .onboarding-container,
+ .stepper-diamond,
+ .onboarding-step-card,
+ .onboarding-btn-secondary,
+ .onboarding-btn-back,
+ .onboarding-btn-primary,
+ .dependency-git-identity-field input,
+ .onboarding-close-btn {
+ animation: none !important;
+ transition: none !important;
+ }
}
diff --git a/client/styles/tabs.css b/client/styles/tabs.css
index 970f58cc..044628f4 100644
--- a/client/styles/tabs.css
+++ b/client/styles/tabs.css
@@ -11,6 +11,8 @@
z-index: 100;
overflow-x: auto;
overflow-y: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.workspace-tabs {
@@ -233,20 +235,27 @@
/* Scrollbar for tab overflow */
.workspace-tabs-container::-webkit-scrollbar {
- height: 4px;
+ height: var(--scrollbar-size);
}
.workspace-tabs-container::-webkit-scrollbar-track {
- background: var(--bg-secondary);
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
}
.workspace-tabs-container::-webkit-scrollbar-thumb {
- background: var(--border-color);
- border-radius: 2px;
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
}
.workspace-tabs-container::-webkit-scrollbar-thumb:hover {
- background: var(--text-secondary);
+ background: var(--scrollbar-thumb-hover);
+}
+
+.workspace-tabs-container::-webkit-scrollbar-thumb:active {
+ background: var(--scrollbar-thumb-active);
}
/* Mobile/Small Screen Adjustments */
diff --git a/server/diagnosticsService.js b/server/diagnosticsService.js
index 7021c243..3761cc3e 100644
--- a/server/diagnosticsService.js
+++ b/server/diagnosticsService.js
@@ -1,22 +1,66 @@
const os = require('os');
const fs = require('fs');
const path = require('path');
-const util = require('util');
-const { execFile } = require('child_process');
-
-const execFileAsync = util.promisify(execFile);
+const { spawn } = require('child_process');
+
+const IS_WIN = process.platform === 'win32';
+const CREATE_NO_WINDOW = 0x08000000;
+
+function execQuiet(command, args, options = {}) {
+ const timeout = Number(options.timeout) || 2500;
+ const maxBuffer = options.maxBuffer || 1024 * 1024;
+ return new Promise((resolve, reject) => {
+ const cmdStr = String(command || '').trim();
+ const argsArr = Array.isArray(args) ? args : [];
+ // On Windows, route .cmd/.bat through cmd.exe directly to avoid retry flashing
+ let spawnCmd = cmdStr;
+ let spawnArgs = argsArr;
+ if (IS_WIN && /\.(cmd|bat)$/i.test(cmdStr)) {
+ spawnCmd = 'cmd.exe';
+ spawnArgs = ['/d', '/c', cmdStr, ...argsArr];
+ }
+ const child = spawn(spawnCmd, spawnArgs, {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ windowsHide: true,
+ ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {})
+ });
+ let stdout = '';
+ let stderr = '';
+ let killed = false;
+ const timer = setTimeout(() => { killed = true; child.kill(); }, timeout);
+ child.stdout.on('data', (d) => {
+ stdout += d;
+ if (stdout.length > maxBuffer) { killed = true; child.kill(); }
+ });
+ child.stderr.on('data', (d) => {
+ stderr += d;
+ if (stderr.length > maxBuffer) { killed = true; child.kill(); }
+ });
+ child.on('error', (err) => { clearTimeout(timer); reject(err); });
+ child.on('close', (code) => {
+ clearTimeout(timer);
+ if (killed) return reject(Object.assign(new Error('TIMEOUT'), { code: 'ETIMEDOUT' }));
+ if (code !== 0) return reject(Object.assign(new Error(stderr || `Exit code ${code}`), { code: 'EXIT', exitCode: code }));
+ resolve({ stdout, stderr });
+ });
+ });
+}
async function checkCommand(command, args, options = {}) {
const timeout = Number(options.timeoutMs) || 2500;
try {
- const { stdout, stderr } = await execFileAsync(command, args, {
- timeout,
- windowsHide: true,
- maxBuffer: 1024 * 1024
- });
+ const result = await execQuiet(command, args, { timeout, maxBuffer: 1024 * 1024 });
+
+ const { stdout, stderr } = result || {};
const output = String(stdout || stderr || '').trim();
const firstLine = output.split(/\r?\n/).find(Boolean) || '';
- return { ok: true, command, args, version: firstLine || null };
+ return {
+ ok: true,
+ command,
+ args,
+ version: firstLine || null,
+ output: output || null
+ };
} catch (error) {
const code = error?.code || null;
const message = String(error?.message || error || '').trim();
@@ -35,6 +79,86 @@ async function checkFirstAvailable(candidates) {
return await checkCommand(last.command, last.args, last.options);
}
+function uniqueCommandCandidates(candidates = []) {
+ const seen = new Set();
+ const out = [];
+ for (const candidate of candidates) {
+ const command = String(candidate?.command || '').trim();
+ if (!command) continue;
+ const args = Array.isArray(candidate?.args) ? candidate.args : [];
+ const key = `${command}::${JSON.stringify(args)}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.push({ command, args, options: candidate?.options });
+ }
+ return out;
+}
+
+async function checkNpmGlobalPackage(npmCommand, packageName) {
+ const npm = String(npmCommand || '').trim();
+ const pkg = String(packageName || '').trim();
+ if (!npm || !pkg) {
+ return { ok: false, error: 'Missing npm command or package name' };
+ }
+
+ const res = await checkCommand(npm, ['list', '-g', pkg, '--depth=0'], { timeoutMs: 7000 });
+ const combined = String(res?.output || res?.version || '').trim();
+ const pkgPattern = new RegExp(`${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@([^\\s]+)`, 'i');
+ const versionMatch = combined.match(pkgPattern);
+ if (!res.ok || !versionMatch?.[1]) {
+ return {
+ ok: false,
+ command: npm,
+ args: ['list', '-g', pkg, '--depth=0'],
+ error: String(res?.error || `Package ${pkg} not found in npm global list`)
+ };
+ }
+
+ return {
+ ok: true,
+ command: `npm-global:${pkg}`,
+ args: ['list', '-g', pkg, '--depth=0'],
+ version: `${pkg}@${versionMatch[1]} (npm global)`
+ };
+}
+
+async function checkGitIdentity(gitCommand, gitInstalled) {
+ const command = String(gitCommand || 'git').trim() || 'git';
+ if (!gitInstalled) {
+ return {
+ ok: false,
+ command,
+ args: ['config', '--global', '--get', 'user.name'],
+ error: 'Git is not installed'
+ };
+ }
+
+ const nameCheck = await checkCommand(command, ['config', '--global', '--get', 'user.name']);
+ const emailCheck = await checkCommand(command, ['config', '--global', '--get', 'user.email']);
+ const name = String(nameCheck?.version || '').trim();
+ const email = String(emailCheck?.version || '').trim();
+
+ if (name && email) {
+ return {
+ ok: true,
+ command,
+ args: ['config', '--global', '--get', 'user.name,user.email'],
+ version: `${name} <${email}>`
+ };
+ }
+
+ const missing = [];
+ if (!name) missing.push('user.name');
+ if (!email) missing.push('user.email');
+
+ return {
+ ok: false,
+ command,
+ args: ['config', '--global', '--get', 'user.name,user.email'],
+ error: `Missing global Git setting(s): ${missing.join(', ')}`
+ };
+}
+
function findTool(tools, id) {
if (!Array.isArray(tools)) return null;
return tools.find((tool) => String(tool?.id || '') === String(id || '')) || null;
@@ -86,47 +210,128 @@ async function collectDiagnostics() {
const tools = [];
+ const nodeCandidates = uniqueCommandCandidates([
+ { command: 'node', args: ['--version'] },
+ { command: platform === 'win32' ? 'node.exe' : 'node', args: ['--version'] },
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ { command: process.execPath || 'node', args: ['--version'] }
+ ]);
+ const nodeCheck = await checkFirstAvailable(nodeCandidates);
+ const nodeCommand = String(nodeCheck?.command || '').trim();
+ const nodeDir = nodeCommand ? path.dirname(nodeCommand) : '';
+
+ const npmCandidates = uniqueCommandCandidates([
+ { command: platform === 'win32' ? 'npm.cmd' : 'npm', args: ['--version'] },
+ platform === 'win32' ? { command: 'npm', args: ['--version'] } : null,
+ platform === 'win32' && nodeDir ? { command: path.join(nodeDir, 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'npm.cmd'), args: ['--version'] } : null
+ ]);
+ const npmCheck = await checkFirstAvailable(npmCandidates);
+
tools.push({
id: 'node',
name: 'Node.js',
- ...(await checkCommand(process.execPath || 'node', ['--version']))
+ ...nodeCheck
});
tools.push({
id: 'npm',
name: 'npm',
- ...(await checkCommand(platform === 'win32' ? 'npm.cmd' : 'npm', ['--version']))
+ ...npmCheck
});
+ const gitCandidates = uniqueCommandCandidates([
+ { command: 'git', args: ['--version'] },
+ platform === 'win32' ? { command: 'git.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null
+ ]);
+
tools.push({
id: 'git',
name: 'Git',
- ...(await checkCommand('git', ['--version']))
+ ...(await checkFirstAvailable(gitCandidates))
+ });
+ const gitTool = tools[tools.length - 1];
+ tools.push({
+ id: 'gitIdentity',
+ name: 'Git identity',
+ ...(await checkGitIdentity(gitTool?.command, !!gitTool?.ok))
});
+ const ghCandidates = uniqueCommandCandidates([
+ { command: 'gh', args: ['--version'] },
+ platform === 'win32' ? { command: 'gh.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null
+ ]);
+ const ghCheck = await checkFirstAvailable(ghCandidates);
tools.push({
id: 'gh',
name: 'GitHub CLI',
- ...(await checkCommand('gh', ['--version']))
+ ...ghCheck
});
// Auth status is the most common root cause of "0 files/commits" in PR tooling on Windows.
// We keep it lightweight: first line of `gh auth status` is enough to spot "not logged in".
+ const ghAuthCheck = ghCheck?.ok
+ ? await checkCommand(String(ghCheck.command || 'gh'), ['auth', 'status'])
+ : {
+ ok: false,
+ command: String(ghCheck?.command || 'gh'),
+ args: ['auth', 'status'],
+ error: 'GitHub CLI is not installed'
+ };
tools.push({
id: 'ghAuth',
name: 'GitHub CLI auth',
- ...(await checkCommand('gh', ['auth', 'status']))
+ ...ghAuthCheck
});
+ const claudeCandidates = uniqueCommandCandidates([
+ { command: 'claude', args: ['--version'] },
+ platform === 'win32' ? { command: 'claude.cmd', args: ['--version'] } : null,
+ platform === 'win32' ? { command: 'claude.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.claude', 'local', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Claude', 'claude.exe'), args: ['--version'] } : null
+ ]);
tools.push({
id: 'claude',
name: 'Claude Code',
- ...(await checkCommand('claude', ['--version']))
+ ...(await checkFirstAvailable(claudeCandidates))
});
+ const codexCandidates = uniqueCommandCandidates([
+ { command: 'codex', args: ['--version'] },
+ platform === 'win32' ? { command: 'codex.cmd', args: ['--version'] } : null,
+ platform === 'win32' ? { command: 'codex.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'codex.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'codex.exe'), args: ['--version'] } : null
+ ]);
+ let codexCheck = await checkFirstAvailable(codexCandidates);
+ if (!codexCheck?.ok && npmCheck?.ok) {
+ const npmPackageCheck = await checkNpmGlobalPackage(String(npmCheck.command || '').trim(), '@openai/codex');
+ if (npmPackageCheck?.ok) {
+ codexCheck = npmPackageCheck;
+ }
+ }
tools.push({
id: 'codex',
name: 'Codex CLI',
- ...(await checkCommand('codex', ['--version']))
+ ...codexCheck
});
tools.push({
@@ -568,11 +773,9 @@ async function runFirstRunRepair({ action, rootDir, homeDir } = {}) {
}
if (actionId === 'rebuild-node-pty') {
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
- const { stdout, stderr } = await execFileAsync(npmCmd, ['rebuild', 'node-pty'], {
- cwd: resolvedRoot,
+ const npmCmd = IS_WIN ? 'npm.cmd' : 'npm';
+ const { stdout, stderr } = await execQuiet(npmCmd, ['rebuild', 'node-pty'], {
timeout: 180000,
- windowsHide: true,
maxBuffer: 4 * 1024 * 1024
});
const output = String(stdout || stderr || '').trim();
diff --git a/server/index.js b/server/index.js
index e3a9a62b..b444dd5c 100644
--- a/server/index.js
+++ b/server/index.js
@@ -99,6 +99,14 @@ const voiceCommandService = require('./voiceCommandService');
const whisperService = require('./whisperService');
const sessionRecoveryService = require('./sessionRecoveryService');
const { collectDiagnostics, collectFirstRunDiagnostics, collectInstallWizard, runFirstRunRepair, runFirstRunSafeRepairs } = require('./diagnosticsService');
+const {
+ getSetupActions,
+ runSetupAction,
+ getSetupActionRun,
+ getLatestSetupActionRun,
+ configureGitIdentity
+} = require('./setupActionService');
+const { OnboardingStateService } = require('./onboardingStateService');
const { PluginLoaderService } = require('./pluginLoaderService');
const { SchedulerService } = require('./schedulerService');
const { PagerService } = require('./pagerService');
@@ -203,6 +211,20 @@ app.use((req, res, next) => {
});
// Define specific routes BEFORE static file serving
+app.get('/bootstrap/setup-state.js', (req, res) => {
+ try {
+ const state = onboardingStateService.getDependencySetupState();
+ res.type('application/javascript');
+ res.set('Cache-Control', 'no-store');
+ res.send(`window.__ORCHESTRATOR_SETUP_STATE__ = ${JSON.stringify(state)};`);
+ } catch (error) {
+ logger.error('Failed to serve setup bootstrap state', { error: error.message, stack: error.stack });
+ res.type('application/javascript');
+ res.set('Cache-Control', 'no-store');
+ res.send('window.__ORCHESTRATOR_SETUP_STATE__ = null;');
+ }
+});
+
// Serve the UI as default
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../client/index.html'));
@@ -295,6 +317,7 @@ const processTaskService = ProcessTaskService.getInstance({ sessionManager, work
const taskRecordService = TaskRecordService.getInstance();
const userSettingsService = UserSettingsService.getInstance();
const licenseService = LicenseService.getInstance();
+const onboardingStateService = OnboardingStateService.getInstance({ logger });
const proOnly = requirePro(licenseService);
const processStatusService = ProcessStatusService.getInstance({ processTaskService, taskRecordService, sessionManager, workspaceManager, userSettingsService });
const processTelemetryService = ProcessTelemetryService.getInstance({ taskRecordService });
@@ -400,6 +423,7 @@ sessionManager.setGitHelper(gitHelper);
// Initialize workspace system
let workspaceInitialized = false;
+let workspaceSystemReady = null;
async function initializeWorkspaceSystem() {
try {
logger.info('Initializing workspace system...');
@@ -425,21 +449,33 @@ async function initializeWorkspaceSystem() {
}
// Initialize workspace system before starting server
-initializeWorkspaceSystem().then(() => {
- logger.info('Workspace system initialized');
- loadPlugins()
- .then((status) => {
- logger.info('Plugin loader finished', {
- loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0,
- failed: Array.isArray(status?.failed) ? status.failed.length : 0
+workspaceSystemReady = initializeWorkspaceSystem()
+ .then(() => {
+ logger.info('Workspace system initialized');
+ return true;
+ })
+ .catch(error => {
+ logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack });
+ return false;
+ });
+
+workspaceSystemReady
+ .then((workspaceReady) => {
+ if (!workspaceReady) return null;
+ return loadPlugins()
+ .then((status) => {
+ logger.info('Plugin loader finished', {
+ loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0,
+ failed: Array.isArray(status?.failed) ? status.failed.length : 0
+ });
+ return status;
+ })
+ .catch((error) => {
+ logger.error('Plugin loader failed', { error: error.message, stack: error.stack });
+ return null;
});
- })
- .catch((error) => {
- logger.error('Plugin loader failed', { error: error.message, stack: error.stack });
- });
-}).catch(error => {
- logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack });
-});
+ })
+ .catch(() => null);
// WebSocket connection handling
io.on('connection', (socket) => {
@@ -2303,9 +2339,7 @@ app.post('/api/files/sync', async (req, res) => {
});
app.get('/api/process/performance', async (req, res) => {
- const { execFile } = require('child_process');
- const util = require('util');
- const execFileAsync = util.promisify(execFile);
+ const { spawn: spawnProc } = require('child_process');
const isWin = process.platform === 'win32';
const parseIntSafe = (s) => {
@@ -2313,15 +2347,27 @@ app.get('/api/process/performance', async (req, res) => {
return Number.isFinite(n) ? Math.round(n) : null;
};
+ const spawnQuiet = (cmd, args, timeout = 1500) => new Promise((resolve) => {
+ const child = spawnProc(cmd, args, {
+ stdio: ['ignore', 'pipe', 'ignore'],
+ windowsHide: true,
+ ...(isWin ? { creationFlags: 0x08000000 } : {})
+ });
+ let out = '';
+ child.stdout.on('data', (d) => { out += d; });
+ const timer = setTimeout(() => { child.kill(); resolve(''); }, timeout);
+ child.on('close', () => { clearTimeout(timer); resolve(out); });
+ child.on('error', () => { clearTimeout(timer); resolve(''); });
+ });
+
const getChildPids = async (pid) => {
const p = Number(pid);
if (!Number.isFinite(p) || p <= 0) return [];
try {
if (isWin) {
- const { stdout } = await execFileAsync(
+ const stdout = await spawnQuiet(
'powershell.exe',
- ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${p}").ProcessId`],
- { timeout: 1500, windowsHide: true }
+ ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${p}").ProcessId`]
);
return String(stdout || '')
.split(/\s+/)
@@ -2329,7 +2375,7 @@ app.get('/api/process/performance', async (req, res) => {
.filter(n => Number.isFinite(n) && n > 0);
}
- const { stdout } = await execFileAsync('pgrep', ['-P', String(p)], { timeout: 1500, windowsHide: true });
+ const stdout = await spawnQuiet('pgrep', ['-P', String(p)]);
return String(stdout || '')
.split('\n')
.map(l => parseIntSafe(l))
@@ -2344,17 +2390,16 @@ app.get('/api/process/performance', async (req, res) => {
if (!Number.isFinite(p) || p <= 0) return null;
try {
if (isWin) {
- const { stdout } = await execFileAsync(
+ const stdout = await spawnQuiet(
'powershell.exe',
- ['-NoProfile', '-Command', `(Get-Process -Id ${p} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty WorkingSet64)`],
- { timeout: 1500, windowsHide: true }
+ ['-NoProfile', '-Command', `(Get-Process -Id ${p} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty WorkingSet64)`]
);
const bytes = Number(String(stdout || '').trim());
if (!Number.isFinite(bytes) || bytes <= 0) return null;
return Math.round(bytes / 1024);
}
- const { stdout } = await execFileAsync('ps', ['-o', 'rss=', '-p', String(p)], { timeout: 1500, windowsHide: true });
+ const stdout = await spawnQuiet('ps', ['-o', 'rss=', '-p', String(p)]);
return parseIntSafe(stdout);
} catch {
return null;
@@ -4050,6 +4095,144 @@ app.get('/api/lifecycle/policy', (req, res) => {
}
});
+// Setup helper actions for first-run dependency wizard.
+app.get('/api/setup-actions', (req, res) => {
+ try {
+ const platform = process.platform;
+ const actions = getSetupActions(platform);
+ res.json({ ok: true, platform, actions });
+ } catch (error) {
+ logger.error('Failed to get setup actions', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to get setup actions' });
+ }
+});
+
+app.get('/api/setup-actions/state', (req, res) => {
+ try {
+ const state = onboardingStateService.getDependencySetupState();
+ res.json({ ok: true, state });
+ } catch (error) {
+ logger.error('Failed to get setup action state', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to get setup action state' });
+ }
+});
+
+app.put('/api/setup-actions/state', express.json(), (req, res) => {
+ try {
+ const patch = (req.body && typeof req.body === 'object') ? req.body : {};
+ const state = onboardingStateService.updateDependencySetupState(patch);
+ res.json({ ok: true, state });
+ } catch (error) {
+ logger.error('Failed to update setup action state', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to update setup action state' });
+ }
+});
+
+app.post('/api/setup-actions/run', requirePolicyAction('write'), express.json(), (req, res) => {
+ try {
+ const actionId = String(req.body?.actionId || '').trim();
+ if (!actionId) {
+ return res.status(400).json({ ok: false, error: 'actionId is required' });
+ }
+
+ const result = runSetupAction(actionId, process.platform);
+ res.json({ ok: true, ...result });
+ } catch (error) {
+ const code = String(error?.code || '');
+ const status = (code === 'unsupported_platform' || code === 'unknown_action' || code === 'not_runnable') ? 400 : 500;
+ logger.error('Failed to run setup action', { actionId: req.body?.actionId, error: error.message, stack: error.stack });
+ res.status(status).json({ ok: false, error: String(error?.message || 'Failed to run setup action') });
+ }
+});
+
+app.post('/api/setup-actions/configure-git-identity', requirePolicyAction('write'), express.json(), async (req, res) => {
+ try {
+ const name = String(req.body?.name || '').trim();
+ const email = String(req.body?.email || '').trim();
+ const result = await configureGitIdentity({ name, email }, process.platform);
+ res.json({ ok: true, ...result });
+ } catch (error) {
+ const code = String(error?.code || '');
+ const status = (
+ code === 'unsupported_platform'
+ || code === 'invalid_input'
+ || code === 'missing_git'
+ || code === 'verify_failed'
+ ) ? 400 : 500;
+ logger.error('Failed to configure git identity', {
+ error: error.message,
+ stack: error.stack
+ });
+ res.status(status).json({ ok: false, error: String(error?.message || 'Failed to configure git identity') });
+ }
+});
+
+app.get('/api/setup-actions/run-status', (req, res) => {
+ try {
+ const runId = String(req.query?.runId || '').trim();
+ const actionId = String(req.query?.actionId || '').trim();
+ const run = runId ? getSetupActionRun(runId) : getLatestSetupActionRun(actionId);
+ if (!run) {
+ return res.status(404).json({ ok: false, error: 'Setup action run not found' });
+ }
+ res.json({ ok: true, run });
+ } catch (error) {
+ logger.error('Failed to get setup action run status', {
+ runId: req.query?.runId,
+ actionId: req.query?.actionId,
+ error: error.message,
+ stack: error.stack
+ });
+ res.status(500).json({ ok: false, error: 'Failed to get setup action run status' });
+ }
+});
+
+app.post('/api/setup-actions/open-url', requirePolicyAction('write'), express.json(), (req, res) => {
+ try {
+ const rawUrl = String(req.body?.url || '').trim();
+ if (!rawUrl) {
+ return res.status(400).json({ ok: false, error: 'url is required' });
+ }
+
+ let parsed;
+ try {
+ parsed = new URL(rawUrl);
+ } catch {
+ return res.status(400).json({ ok: false, error: 'Invalid URL' });
+ }
+
+ if (!['http:', 'https:'].includes(String(parsed.protocol || '').toLowerCase())) {
+ return res.status(400).json({ ok: false, error: 'Only http/https URLs are supported' });
+ }
+
+ const targetUrl = parsed.toString();
+ const { execFile } = require('child_process');
+
+ const finish = (error) => {
+ if (error) {
+ logger.error('Failed to open setup URL', { url: targetUrl, error: error.message, stack: error.stack });
+ return res.status(500).json({ ok: false, error: 'Failed to open URL' });
+ }
+ res.json({ ok: true, opened: targetUrl });
+ };
+
+ if (process.platform === 'win32') {
+ execFile('explorer.exe', [targetUrl], { windowsHide: true }, finish);
+ return;
+ }
+
+ if (process.platform === 'darwin') {
+ execFile('open', [targetUrl], { windowsHide: true }, finish);
+ return;
+ }
+
+ execFile('xdg-open', [targetUrl], { windowsHide: true }, finish);
+ } catch (error) {
+ logger.error('Failed to open setup URL', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to open URL' });
+ }
+});
+
// Port registry API endpoints
app.get('/api/ports', (req, res) => {
try {
@@ -7836,7 +8019,13 @@ httpServer.listen(PORT, HOST, () => {
}
})();
- sessionManager.initializeSessions()
+ workspaceSystemReady
+ .then((workspaceReady) => {
+ if (!workspaceReady) {
+ return;
+ }
+ return sessionManager.initializeSessions();
+ })
.then(() => {
if (!shouldAutoEnsureDiscordServices) return;
// Donโt block server startup; just best-effort keep Services running after restarts.
diff --git a/server/onboardingStateService.js b/server/onboardingStateService.js
new file mode 100644
index 00000000..a6d43031
--- /dev/null
+++ b/server/onboardingStateService.js
@@ -0,0 +1,120 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const STATE_VERSION = 1;
+
+class OnboardingStateService {
+ constructor({ logger = console, storePath = null } = {}) {
+ this.logger = logger;
+ this.storePath = storePath ? path.resolve(String(storePath)) : this.resolveStorePath();
+ }
+
+ static getInstance(options = {}) {
+ if (!OnboardingStateService.instance) {
+ OnboardingStateService.instance = new OnboardingStateService(options);
+ }
+ return OnboardingStateService.instance;
+ }
+
+ resolveStorePath() {
+ const dataDirRaw = String(process.env.ORCHESTRATOR_DATA_DIR || '').trim();
+ const baseDir = dataDirRaw ? path.resolve(dataDirRaw) : path.join(os.homedir(), '.orchestrator');
+ try {
+ if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
+ } catch {
+ // ignore
+ }
+ return path.join(baseDir, 'onboarding-state.json');
+ }
+
+ getDefaultState() {
+ return {
+ version: STATE_VERSION,
+ updatedAt: null,
+ dependencySetup: {
+ completed: false,
+ dismissed: false,
+ currentStep: 0,
+ skippedActionIds: []
+ }
+ };
+ }
+
+ normalizeSkippedActionIds(value) {
+ if (!Array.isArray(value)) return [];
+ const seen = new Set();
+ const result = [];
+ for (const rawId of value) {
+ const id = String(rawId || '').trim();
+ if (!id || seen.has(id)) continue;
+ seen.add(id);
+ result.push(id);
+ }
+ return result;
+ }
+
+ normalizeDependencySetupState(value) {
+ const next = (value && typeof value === 'object') ? value : {};
+ const currentStepRaw = Number.parseInt(String(next.currentStep ?? 0), 10);
+ return {
+ completed: next.completed === true,
+ dismissed: next.dismissed === true,
+ currentStep: Number.isFinite(currentStepRaw) && currentStepRaw >= 0 ? currentStepRaw : 0,
+ skippedActionIds: this.normalizeSkippedActionIds(next.skippedActionIds)
+ };
+ }
+
+ loadState() {
+ const defaults = this.getDefaultState();
+ try {
+ if (!fs.existsSync(this.storePath)) {
+ return defaults;
+ }
+ const parsed = JSON.parse(fs.readFileSync(this.storePath, 'utf8'));
+ return {
+ version: STATE_VERSION,
+ updatedAt: typeof parsed?.updatedAt === 'string' ? parsed.updatedAt : null,
+ dependencySetup: this.normalizeDependencySetupState(parsed?.dependencySetup)
+ };
+ } catch (error) {
+ this.logger.warn?.('Failed to load onboarding state', {
+ path: this.storePath,
+ error: error.message
+ });
+ return defaults;
+ }
+ }
+
+ saveState(state) {
+ const normalized = {
+ version: STATE_VERSION,
+ updatedAt: new Date().toISOString(),
+ dependencySetup: this.normalizeDependencySetupState(state?.dependencySetup)
+ };
+ const dir = path.dirname(this.storePath);
+ fs.mkdirSync(dir, { recursive: true });
+ const tmpPath = `${this.storePath}.tmp`;
+ fs.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2));
+ fs.renameSync(tmpPath, this.storePath);
+ return normalized;
+ }
+
+ getDependencySetupState() {
+ return this.loadState().dependencySetup;
+ }
+
+ updateDependencySetupState(patch = {}) {
+ const current = this.loadState();
+ const next = {
+ ...current,
+ dependencySetup: this.normalizeDependencySetupState({
+ ...(current?.dependencySetup || {}),
+ ...((patch && typeof patch === 'object') ? patch : {})
+ })
+ };
+ return this.saveState(next).dependencySetup;
+ }
+}
+
+module.exports = { OnboardingStateService };
diff --git a/server/sessionManager.js b/server/sessionManager.js
index d439f289..f9efc646 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -2357,15 +2357,27 @@ class SessionManager extends EventEmitter {
checkProcessLimit(session) {
if (!session.pty || !session.pty.pid) return;
-
+
const pid = Number(session.pty.pid);
if (!Number.isFinite(pid) || pid <= 0) return;
+ const { spawn } = require('child_process');
+
if (process.platform === 'win32') {
- const { execFile } = require('child_process');
const psCmd = `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}").Count`;
- execFile('powershell.exe', ['-NoProfile', '-Command', psCmd], { timeout: 2000 }, (err, stdout) => {
- if (err) return;
+ const child = spawn('powershell.exe', ['-NoProfile', '-Command', psCmd], {
+ stdio: ['ignore', 'pipe', 'ignore'],
+ windowsHide: true,
+ creationFlags: 0x08000000 // CREATE_NO_WINDOW
+ });
+ let stdout = '';
+ child.stdout.on('data', (d) => { stdout += d; });
+ const timer = setTimeout(() => child.kill(), 2000);
+ child.on('error', () => {
+ clearTimeout(timer);
+ });
+ child.on('close', () => {
+ clearTimeout(timer);
const processCount = parseInt(String(stdout || '').trim(), 10);
if (!Number.isFinite(processCount)) return;
if (processCount > this.maxProcessesPerSession) {
@@ -2381,10 +2393,20 @@ class SessionManager extends EventEmitter {
}
// POSIX: use pgrep to count child processes without shell interpolation.
- const { execFile } = require('child_process');
- execFile('pgrep', ['-P', String(pid)], { timeout: 2000, windowsHide: true }, (err, stdout) => {
+ const child = spawn('pgrep', ['-P', String(pid)], {
+ stdio: ['ignore', 'pipe', 'ignore'],
+ windowsHide: true
+ });
+ let stdout = '';
+ child.stdout.on('data', (d) => { stdout += d; });
+ const timer = setTimeout(() => child.kill(), 2000);
+ child.on('error', () => {
+ clearTimeout(timer);
+ });
+ child.on('close', (code) => {
+ clearTimeout(timer);
// pgrep exits with code 1 when no child process matches; treat as zero children.
- if (err && Number(err?.code) !== 1) return;
+ if (code !== 0 && code !== 1) return;
const lines = String(stdout || '')
.split(/\r?\n/)
.map((line) => line.trim())
@@ -2392,7 +2414,7 @@ class SessionManager extends EventEmitter {
const processCount = lines.length;
if (!Number.isFinite(processCount)) return;
if (processCount > this.maxProcessesPerSession) {
- logger.error('Process limit exceeded', {
+ logger.error('Process limit exceeded', {
sessionId: session.id,
processCount,
limit: this.maxProcessesPerSession
@@ -2468,13 +2490,13 @@ class SessionManager extends EventEmitter {
if (!Number.isFinite(numericPid) || numericPid <= 0) return;
if (process.platform === 'win32') {
- const { execFile } = require('child_process');
- execFile(
- 'taskkill',
- ['/PID', String(numericPid), '/T', '/F'],
- { windowsHide: true, timeout: 2500 },
- () => {}
- );
+ const { spawn: spawnProc } = require('child_process');
+ const child = spawnProc('taskkill', ['/PID', String(numericPid), '/T', '/F'], {
+ stdio: 'ignore',
+ windowsHide: true,
+ creationFlags: 0x08000000
+ });
+ child.on('error', () => {});
return;
}
diff --git a/server/setupActionService.js b/server/setupActionService.js
new file mode 100644
index 00000000..4ad5e234
--- /dev/null
+++ b/server/setupActionService.js
@@ -0,0 +1,568 @@
+const crypto = require('crypto');
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+const { spawn } = require('child_process');
+
+const IS_WIN = process.platform === 'win32';
+const CREATE_NO_WINDOW = 0x08000000;
+
+function execQuiet(command, args, options = {}) {
+ const timeout = Number(options.timeout) || 3000;
+ const maxBuffer = options.maxBuffer || 1024 * 1024;
+ return new Promise((resolve, reject) => {
+ const cmdStr = String(command || '').trim();
+ const argsArr = Array.isArray(args) ? args : [];
+ let spawnCmd = cmdStr;
+ let spawnArgs = argsArr;
+ if (IS_WIN && /\.(cmd|bat)$/i.test(cmdStr)) {
+ spawnCmd = 'cmd.exe';
+ spawnArgs = ['/d', '/c', cmdStr, ...argsArr];
+ }
+ const child = spawn(spawnCmd, spawnArgs, {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ windowsHide: true,
+ ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {})
+ });
+ let stdout = '';
+ let stderr = '';
+ let killed = false;
+ const timer = setTimeout(() => { killed = true; child.kill(); }, timeout);
+ child.stdout.on('data', (d) => {
+ stdout += d;
+ if (stdout.length > maxBuffer) { killed = true; child.kill(); }
+ });
+ child.stderr.on('data', (d) => {
+ stderr += d;
+ if (stderr.length > maxBuffer) { killed = true; child.kill(); }
+ });
+ child.on('error', (err) => { clearTimeout(timer); reject(err); });
+ child.on('close', (code) => {
+ clearTimeout(timer);
+ if (killed) return reject(Object.assign(new Error('TIMEOUT'), { code: 'ETIMEDOUT' }));
+ if (code !== 0) return reject(Object.assign(new Error(stderr || `Exit code ${code}`), { code: 'EXIT', exitCode: code }));
+ resolve({ stdout, stderr });
+ });
+ });
+}
+
+const setupActionRuns = new Map();
+const latestRunByActionId = new Map();
+const MAX_OUTPUT_LINES = 180;
+const MAX_RUNS_RETAINED = 50;
+
+function pruneOldRuns() {
+ if (setupActionRuns.size <= MAX_RUNS_RETAINED) return;
+ const toDelete = Array.from(setupActionRuns.keys()).slice(0, setupActionRuns.size - MAX_RUNS_RETAINED);
+ for (const key of toDelete) {
+ setupActionRuns.delete(key);
+ }
+}
+const GH_LOGIN_CODE_PATTERN = /\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i;
+const GH_LOGIN_URL_PATTERN = /https:\/\/github\.com\/login\/device(?:\S*)?/i;
+const GH_LOGIN_HINT_PATTERN = /one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i;
+
+function stripAnsi(value) {
+ return String(value || '').replace(/\u001b\[[0-9;]*m/g, '');
+}
+
+function getGhLoginDebugLogPath() {
+ const customDataDir = String(process.env.ORCHESTRATOR_DATA_DIR || '').trim();
+ if (customDataDir) {
+ return path.join(customDataDir, 'logs', 'gh-login-debug.log');
+ }
+ return path.join(os.tmpdir(), 'orchestrator-gh-login-debug.log');
+}
+
+function appendGhLoginDebugLog(event, payload = {}) {
+ const line = JSON.stringify({
+ at: new Date().toISOString(),
+ event: String(event || '').trim() || 'event',
+ ...(payload && typeof payload === 'object' ? payload : {})
+ });
+ const logPath = getGhLoginDebugLogPath();
+ try {
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
+ fs.appendFileSync(logPath, `${line}\n`, 'utf8');
+ } catch {
+ // Best-effort debug logging; never block setup flow.
+ }
+}
+
+function uniqueStrings(values = []) {
+ const seen = new Set();
+ const out = [];
+ values.forEach((value) => {
+ const item = String(value || '').trim();
+ if (!item || seen.has(item)) return;
+ seen.add(item);
+ out.push(item);
+ });
+ return out;
+}
+
+async function checkExecutable(command, args = ['--version']) {
+ const commandStr = String(command || '').trim();
+ if (!commandStr) return { ok: false, error: 'Missing command' };
+
+ try {
+ await execQuiet(commandStr, Array.isArray(args) ? args : [], { timeout: 3000 });
+ return { ok: true };
+ } catch (error) {
+ return {
+ ok: false,
+ error: String(error?.message || error || 'Command check failed')
+ };
+ }
+}
+
+function getGitCommandCandidates(platform = process.platform) {
+ if (platform !== 'win32') {
+ return ['git'];
+ }
+
+ return uniqueStrings([
+ 'git',
+ 'git.exe',
+ path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'),
+ path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'),
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'),
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'),
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe')
+ ]);
+}
+
+async function resolveGitCommand(platform = process.platform) {
+ const candidates = getGitCommandCandidates(platform);
+ for (const command of candidates) {
+ const check = await checkExecutable(command, ['--version']);
+ if (check.ok) return command;
+ }
+ return '';
+}
+
+async function runGitCommand(command, args = []) {
+ try {
+ const result = await execQuiet(command, Array.isArray(args) ? args : [], { timeout: 9000 });
+ return String(result?.stdout || result?.stderr || '');
+ } catch (error) {
+ const message = String(error?.message || error || 'Git command failed');
+ const err = new Error(message);
+ err.code = String(error?.code || 'git_command_failed');
+ throw err;
+ }
+}
+
+function firstNonEmptyLine(text) {
+ return String(text || '')
+ .replace(/\r/g, '')
+ .split('\n')
+ .map((line) => line.trim())
+ .find(Boolean) || '';
+}
+
+function getSetupActions(platform = process.platform) {
+ if (platform !== 'win32') {
+ return [];
+ }
+
+ return [
+ {
+ id: 'install-git',
+ title: 'Git Integration',
+ description: 'Required for repository and worktree access.',
+ command: 'winget install --id Git.Git --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://git-scm.com/download/win',
+ required: true,
+ runSupported: true
+ },
+ {
+ id: 'configure-git-identity',
+ title: 'Git Identity',
+ description: 'Set your name and email for accurate commits.',
+ command: 'git config --global user.name "Your Name"\ngit config --global user.email "you@example.com"',
+ docsUrl: 'https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup',
+ required: false,
+ optional: true,
+ runSupported: false
+ },
+ {
+ id: 'install-node',
+ title: 'Node.js LTS',
+ description: 'Required core dependency for running agents.',
+ command: 'winget install --id OpenJS.NodeJS.LTS --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://nodejs.org/en/download',
+ required: false,
+ runSupported: true
+ },
+ {
+ id: 'install-gh',
+ title: 'GitHub CLI',
+ description: 'Optional. Install now, then continue to GitHub login in the next step.',
+ command: 'winget install --id GitHub.cli --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://cli.github.com/',
+ required: false,
+ optional: true,
+ runSupported: true
+ },
+ {
+ id: 'gh-login',
+ title: 'GitHub Authentication',
+ description: 'Optional after GitHub CLI install. Sign in to enable PR and repo actions.',
+ command: [
+ "$ErrorActionPreference = 'Stop'",
+ '$env:NO_COLOR = "1"',
+ '$env:GH_PAGER = ""',
+ '$gh = ""',
+ '$cmd = Get-Command gh -ErrorAction SilentlyContinue',
+ 'if ($cmd -and $cmd.Source) { $gh = $cmd.Source }',
+ 'if (-not $gh) {',
+ ' $candidates = @(',
+ ' "$env:ProgramFiles\\GitHub CLI\\gh.exe",',
+ ' "$env:ProgramFiles(x86)\\GitHub CLI\\gh.exe",',
+ ' "$env:LOCALAPPDATA\\Programs\\GitHub CLI\\gh.exe"',
+ ' )',
+ ' foreach ($candidate in $candidates) {',
+ ' if (Test-Path $candidate) { $gh = $candidate; break }',
+ ' }',
+ '}',
+ 'if (-not $gh) { throw "GitHub CLI executable not found. Install GitHub CLI first." }',
+ '$prevErrorAction = $ErrorActionPreference',
+ '$ErrorActionPreference = "Continue"',
+ '& $gh auth status --hostname github.com *> $null',
+ '$authStatusExitCode = $LASTEXITCODE',
+ '$ErrorActionPreference = $prevErrorAction',
+ 'if ($authStatusExitCode -eq 0) { Write-Output "GitHub CLI is already authenticated."; exit 0 }',
+ 'Write-Output "Starting GitHub CLI web login..."',
+ 'Write-Output "Expect a one-time code and https://github.com/login/device below."',
+ '& $gh auth login --hostname github.com --git-protocol https --web --skip-ssh-key',
+ 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }'
+ ].join('\n'),
+ docsUrl: 'https://cli.github.com/manual/gh_auth_login',
+ required: false,
+ optional: true,
+ runSupported: true
+ },
+ {
+ id: 'install-claude',
+ title: 'Claude Code CLI',
+ description: 'Primary AI agent powered by Anthropic.',
+ command: 'winget install --id Anthropic.ClaudeCode --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://docs.claude.com/en/docs/claude-code/setup',
+ required: false,
+ optional: true,
+ runSupported: true
+ },
+ {
+ id: 'install-codex',
+ title: 'Codex CLI',
+ description: 'Alternative AI agent tool for development.',
+ command: [
+ "$ErrorActionPreference = 'Stop'",
+ '$npm = ""',
+ '$cmd = Get-Command npm -ErrorAction SilentlyContinue',
+ 'if ($cmd -and $cmd.Source) { $npm = $cmd.Source }',
+ 'if (-not $npm) {',
+ ' $candidates = @(',
+ ' "$env:ProgramFiles\\nodejs\\npm.cmd",',
+ ' "$env:ProgramFiles(x86)\\nodejs\\npm.cmd",',
+ ' "$env:LOCALAPPDATA\\Programs\\nodejs\\npm.cmd",',
+ ' "$env:APPDATA\\npm\\npm.cmd"',
+ ' )',
+ ' foreach ($candidate in $candidates) {',
+ ' if (Test-Path $candidate) { $npm = $candidate; break }',
+ ' }',
+ '}',
+ 'if (-not $npm) { throw "npm was not found. Install Node.js LTS first, then run this step again." }',
+ '& $npm install -g @openai/codex',
+ 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }'
+ ].join('\n'),
+ docsUrl: 'https://developers.openai.com/codex/cli',
+ required: false,
+ runSupported: true
+ }
+ ];
+}
+
+function getSetupActionById(actionId, platform = process.platform) {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ return getSetupActions(platform).find((action) => action.id === id) || null;
+}
+
+function createRunId(actionId) {
+ return `setup-${String(actionId || 'action')}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
+}
+
+function getRunSummary(run) {
+ if (!run) return null;
+ return {
+ runId: run.runId,
+ actionId: run.actionId,
+ title: run.title,
+ command: run.command,
+ status: run.status,
+ startedAt: run.startedAt,
+ endedAt: run.endedAt || null,
+ pid: Number.isFinite(run.pid) ? run.pid : null,
+ exitCode: Number.isInteger(run.exitCode) ? run.exitCode : null,
+ error: run.error || null,
+ output: Array.isArray(run.output) ? run.output.slice(-25) : [],
+ ghDeviceCode: run.ghDeviceCode || null,
+ ghDeviceUrl: run.ghDeviceUrl || null,
+ ghHasDeviceHint: !!run.ghHasDeviceHint,
+ ghDebugLogPath: run.ghDebugLogPath || null,
+ updatedAt: run.updatedAt || run.startedAt
+ };
+}
+
+function appendRunOutput(run, chunk, stream = 'stdout') {
+ if (!run) return;
+ const text = String(chunk || '');
+ if (!text) return;
+ const lines = text
+ .replace(/\r/g, '')
+ .split('\n')
+ .map((line) => stripAnsi(line).trimEnd())
+ .filter(Boolean);
+ if (!lines.length) return;
+ const at = new Date().toISOString();
+ lines.forEach((line) => {
+ const cleanLine = String(line || '').slice(0, 1600);
+ run.output.push({ at, stream, line: cleanLine });
+ if (run.actionId === 'gh-login') {
+ const codeMatch = cleanLine.match(GH_LOGIN_CODE_PATTERN);
+ const urlMatch = cleanLine.match(GH_LOGIN_URL_PATTERN);
+ if (codeMatch?.[1]) run.ghDeviceCode = String(codeMatch[1]).toUpperCase();
+ if (urlMatch?.[0]) run.ghDeviceUrl = String(urlMatch[0]).trim();
+ if (GH_LOGIN_HINT_PATTERN.test(cleanLine)) run.ghHasDeviceHint = true;
+ appendGhLoginDebugLog('output', {
+ runId: run.runId,
+ stream,
+ line: cleanLine
+ });
+ }
+ });
+ if (run.output.length > MAX_OUTPUT_LINES) {
+ run.output.splice(0, run.output.length - MAX_OUTPUT_LINES);
+ }
+ run.updatedAt = at;
+}
+
+function launchPowerShellCommand(action) {
+ const runId = createRunId(action.id);
+ const run = {
+ runId,
+ actionId: action.id,
+ title: action.title,
+ command: action.command,
+ status: 'running',
+ startedAt: new Date().toISOString(),
+ endedAt: null,
+ pid: null,
+ exitCode: null,
+ error: null,
+ output: [],
+ ghDeviceCode: null,
+ ghDeviceUrl: null,
+ ghHasDeviceHint: false,
+ ghDebugLogPath: action.id === 'gh-login' ? getGhLoginDebugLogPath() : null,
+ updatedAt: null
+ };
+ run.updatedAt = run.startedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_started', {
+ runId: run.runId,
+ title: run.title
+ });
+ }
+
+ setupActionRuns.set(runId, run);
+ latestRunByActionId.set(action.id, runId);
+ pruneOldRuns();
+
+ try {
+ const child = spawn(
+ 'powershell.exe',
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', String(action.command || '')],
+ {
+ detached: false,
+ windowsHide: true,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ ...(IS_WIN ? { creationFlags: CREATE_NO_WINDOW } : {})
+ }
+ );
+ run.pid = Number.isFinite(child?.pid) ? child.pid : null;
+ run.updatedAt = new Date().toISOString();
+
+ child.stdout.on('data', (chunk) => appendRunOutput(run, chunk, 'stdout'));
+ child.stderr.on('data', (chunk) => appendRunOutput(run, chunk, 'stderr'));
+
+ child.on('error', (error) => {
+ run.status = 'failed';
+ run.error = String(error?.message || error || 'Failed to launch setup action');
+ run.endedAt = new Date().toISOString();
+ run.updatedAt = run.endedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_error', {
+ runId: run.runId,
+ error: run.error
+ });
+ }
+ });
+
+ child.on('close', (code) => {
+ run.exitCode = Number.isInteger(code) ? code : null;
+ run.status = code === 0 ? 'success' : 'failed';
+ if (code !== 0 && !run.error) {
+ run.error = `Setup action exited with code ${String(code)}`;
+ }
+ run.endedAt = new Date().toISOString();
+ run.updatedAt = run.endedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_closed', {
+ runId: run.runId,
+ status: run.status,
+ exitCode: run.exitCode,
+ error: run.error || null,
+ parsedCode: run.ghDeviceCode || null,
+ parsedUrl: run.ghDeviceUrl || null,
+ sawHint: !!run.ghHasDeviceHint
+ });
+ }
+ });
+ } catch (error) {
+ run.status = 'failed';
+ run.error = String(error?.message || error || 'Failed to launch setup action');
+ run.endedAt = new Date().toISOString();
+ run.updatedAt = run.endedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_launch_failed', {
+ runId: run.runId,
+ error: run.error
+ });
+ }
+ }
+
+ return run;
+}
+
+function getSetupActionRun(runId) {
+ const key = String(runId || '').trim();
+ if (!key) return null;
+ return getRunSummary(setupActionRuns.get(key));
+}
+
+function getLatestSetupActionRun(actionId) {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ const runId = latestRunByActionId.get(id);
+ if (!runId) return null;
+ return getRunSummary(setupActionRuns.get(runId));
+}
+
+function runSetupAction(actionId, platform = process.platform) {
+ if (platform !== 'win32') {
+ const err = new Error('Setup actions are currently implemented for Windows only.');
+ err.code = 'unsupported_platform';
+ throw err;
+ }
+
+ const action = getSetupActionById(actionId, platform);
+ if (!action) {
+ const err = new Error(`Unknown setup action: ${String(actionId || '')}`);
+ err.code = 'unknown_action';
+ throw err;
+ }
+
+ if (!action.runSupported || !action.command) {
+ const err = new Error(`Action "${action.id}" cannot be launched from the app.`);
+ err.code = 'not_runnable';
+ throw err;
+ }
+
+ const latestRun = getLatestSetupActionRun(action.id);
+ if (latestRun && latestRun.status === 'running') {
+ return {
+ id: action.id,
+ title: action.title,
+ started: true,
+ alreadyRunning: true,
+ run: latestRun,
+ message: `${action.title} is already running.`
+ };
+ }
+
+ const run = launchPowerShellCommand(action);
+ const runSummary = getRunSummary(run);
+
+ return {
+ id: action.id,
+ title: action.title,
+ started: true,
+ alreadyRunning: false,
+ run: runSummary,
+ message: `Started ${action.title}. Progress updates are now tracked in onboarding.`
+ };
+}
+
+async function configureGitIdentity({ name, email } = {}, platform = process.platform) {
+ if (platform !== 'win32') {
+ const err = new Error('Git identity setup is currently implemented for Windows only.');
+ err.code = 'unsupported_platform';
+ throw err;
+ }
+
+ const normalizedName = String(name || '').trim();
+ const normalizedEmail = String(email || '').trim();
+ if (!normalizedName || !normalizedEmail) {
+ const err = new Error('Both name and email are required.');
+ err.code = 'invalid_input';
+ throw err;
+ }
+
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
+ const err = new Error('Enter a valid email address.');
+ err.code = 'invalid_input';
+ throw err;
+ }
+
+ const gitCommand = await resolveGitCommand(platform);
+ if (!gitCommand) {
+ const err = new Error('Git is not installed or not available on PATH.');
+ err.code = 'missing_git';
+ throw err;
+ }
+
+ await runGitCommand(gitCommand, ['config', '--global', 'user.name', normalizedName]);
+ await runGitCommand(gitCommand, ['config', '--global', 'user.email', normalizedEmail]);
+
+ const savedName = firstNonEmptyLine(await runGitCommand(gitCommand, ['config', '--global', '--get', 'user.name']));
+ const savedEmail = firstNonEmptyLine(await runGitCommand(gitCommand, ['config', '--global', '--get', 'user.email']));
+
+ if (!savedName || !savedEmail) {
+ const err = new Error('Git identity was saved, but verification failed.');
+ err.code = 'verify_failed';
+ throw err;
+ }
+
+ return {
+ id: 'configure-git-identity',
+ title: 'Configure Git identity',
+ ok: true,
+ gitCommand,
+ name: savedName,
+ email: savedEmail,
+ summary: `${savedName} <${savedEmail}>`,
+ message: 'Git identity saved successfully.'
+ };
+}
+
+module.exports = {
+ getSetupActions,
+ getSetupActionById,
+ runSetupAction,
+ getSetupActionRun,
+ getLatestSetupActionRun,
+ configureGitIdentity
+};
diff --git a/server/workspaceManager.js b/server/workspaceManager.js
index 06c2aaf3..35d550db 100644
--- a/server/workspaceManager.js
+++ b/server/workspaceManager.js
@@ -707,20 +707,31 @@ class WorkspaceManager {
// 3. First available workspace
// 4. None (show dashboard)
- // Don't auto-select workspace - let user choose from dashboard
- // if (this.config.activeWorkspace && this.workspaces.has(this.config.activeWorkspace)) {
- // this.activeWorkspace = this.workspaces.get(this.config.activeWorkspace);
- // logger.info(`Set active workspace from config: ${this.activeWorkspace.name}`);
- // return;
- // }
-
- // Don't auto-select first workspace - show dashboard instead
- // if (this.workspaces.size > 0) {
- // const firstWorkspace = Array.from(this.workspaces.values())[0];
- // this.activeWorkspace = firstWorkspace;
- // logger.info(`Set active workspace (first available): ${this.activeWorkspace.name}`);
- // return;
- // }
+ const rememberLastWorkspace = this.config?.ui?.rememberLastWorkspace !== false;
+ const configuredWorkspaceId = String(this.config?.activeWorkspace || '').trim();
+
+ if (rememberLastWorkspace && configuredWorkspaceId && this.workspaces.has(configuredWorkspaceId)) {
+ this.activeWorkspace = this.workspaces.get(configuredWorkspaceId);
+ logger.info(`Set active workspace from config: ${this.activeWorkspace.name}`);
+ return;
+ }
+
+ if (rememberLastWorkspace && configuredWorkspaceId && !this.workspaces.has(configuredWorkspaceId)) {
+ logger.warn(`Configured active workspace missing: ${configuredWorkspaceId}`);
+ }
+
+ if (rememberLastWorkspace && this.workspaces.size > 0) {
+ const sorted = Array.from(this.workspaces.values())
+ .sort((a, b) => {
+ const aTime = a.lastAccess ? new Date(a.lastAccess).getTime() : 0;
+ const bTime = b.lastAccess ? new Date(b.lastAccess).getTime() : 0;
+ return bTime - aTime;
+ });
+ const firstWorkspace = sorted[0];
+ this.activeWorkspace = firstWorkspace;
+ logger.info(`Set active workspace by fallback: ${this.activeWorkspace.name}`);
+ return;
+ }
logger.info('No active workspace set (no workspaces available)');
}
@@ -1004,7 +1015,7 @@ class WorkspaceManager {
enabled: true,
count: pairs,
namingPattern: 'work{n}',
- autoCreate: false
+ autoCreate: true // auto-create worktrees when workspace is first opened
},
terminals: {
pairs
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 076c0231..d2b2d184 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -10,6 +10,14 @@ use uuid::Uuid;
use tauri_plugin_updater::UpdaterExt;
use url::Url;
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+#[cfg(target_os = "windows")]
+const CREATE_NO_WINDOW: u32 = 0x08000000;
+#[cfg(target_os = "windows")]
+const DETACHED_PROCESS: u32 = 0x00000008;
+
mod terminal;
mod file_watcher;
use terminal::{TerminalManager, TerminalOutput};
@@ -602,6 +610,18 @@ fn main() {
cmd.arg(entry);
cmd.current_dir(&data_dir);
cmd.stdin(Stdio::null());
+ // On Windows, null stdout/stderr to avoid console window flash.
+ // On other platforms, keep stderr for debugging.
+ #[cfg(target_os = "windows")]
+ {
+ cmd.stdout(Stdio::null());
+ cmd.stderr(Stdio::null());
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ cmd.stdout(Stdio::null());
+ cmd.stderr(Stdio::inherit());
+ }
cmd.env("ORCHESTRATOR_HOST", "127.0.0.1");
cmd.env("ORCHESTRATOR_PORT", port.to_string());
cmd.env("AUTH_TOKEN", token.clone());
@@ -612,6 +632,11 @@ fn main() {
cmd.env("AUTO_START_DIFF_VIEWER", "false");
}
+ #[cfg(target_os = "windows")]
+ {
+ cmd.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS);
+ }
+
match cmd.spawn() {
Err(err) => {
let details = format!(
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index e6e4e6c7..629f0e47 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -40,7 +40,13 @@
"resources/backend/*",
"resources/backend/server/*",
"resources/backend/client/*",
+ "resources/backend/node/*",
"resources/backend/node_modules"
]
+ },
+ "plugins": {
+ "updater": {
+ "pubkey": ""
+ }
}
}
diff --git a/tests/unit/diagnosticsService.test.js b/tests/unit/diagnosticsService.test.js
index 571466fd..ab5c98d8 100644
--- a/tests/unit/diagnosticsService.test.js
+++ b/tests/unit/diagnosticsService.test.js
@@ -1,36 +1,88 @@
+const { EventEmitter } = require('events');
+const { Readable } = require('stream');
+
describe('diagnosticsService platform smoke', () => {
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
+ function fakeSpawn(command, args) {
+ const child = new EventEmitter();
+ const stdout = new Readable({ read() {} });
+ const stderr = new Readable({ read() {} });
+ child.stdout = stdout;
+ child.stderr = stderr;
+ child.kill = () => {};
+
+ const cmd = String(command || '');
+ const argv = Array.isArray(args) ? args.map(String) : [];
+
+ // Resolve .cmd wrappers through cmd.exe
+ let resolvedCmd = cmd;
+ if (cmd === 'cmd.exe' && argv[0] === '/d' && argv[1] === '/c') {
+ resolvedCmd = argv[2] || '';
+ }
+
+ process.nextTick(() => {
+ if (resolvedCmd === process.execPath || resolvedCmd === 'node') {
+ stdout.push('v22.0.0\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ } else if (resolvedCmd === 'npm' || resolvedCmd === 'npm.cmd') {
+ stdout.push('10.0.0\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ } else if (resolvedCmd === 'git' || resolvedCmd === 'git.exe') {
+ if (argv.includes('user.name')) {
+ stdout.push('Test User\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ } else if (argv.includes('user.email')) {
+ stdout.push('test@example.com\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ } else {
+ stdout.push('git version 2.44.0\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ }
+ } else if (resolvedCmd === 'gh' && argv.includes('--version')) {
+ stdout.push('gh version 2.61.0\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ } else if (resolvedCmd === 'gh' && argv.includes('auth')) {
+ stdout.push(null); stderr.push('not logged in\n'); stderr.push(null);
+ child.emit('close', 1);
+ } else if (resolvedCmd === 'claude' || resolvedCmd === 'claude.cmd') {
+ stdout.push(null); stderr.push(null);
+ child.emit('error', Object.assign(new Error('missing command: claude'), { code: 'ENOENT' }));
+ } else if (resolvedCmd === 'codex' || resolvedCmd === 'codex.cmd') {
+ stdout.push(null); stderr.push(null);
+ child.emit('error', Object.assign(new Error('missing command: codex'), { code: 'ENOENT' }));
+ } else if (resolvedCmd === 'bash' || resolvedCmd === 'bash.exe' || resolvedCmd === 'powershell.exe') {
+ stdout.push('shell ok\n'); stdout.push(null); stderr.push(null);
+ child.emit('close', 0);
+ } else if (resolvedCmd === 'ffmpeg') {
+ stdout.push(null); stderr.push(null);
+ child.emit('error', Object.assign(new Error('missing command: ffmpeg'), { code: 'ENOENT' }));
+ } else if (resolvedCmd === 'wsl.exe') {
+ stdout.push(null); stderr.push(null);
+ child.emit('error', Object.assign(new Error('missing command: wsl.exe'), { code: 'ENOENT' }));
+ } else {
+ stdout.push(null); stderr.push(null);
+ child.emit('error', Object.assign(new Error(`missing command: ${resolvedCmd}`), { code: 'ENOENT' }));
+ }
+ });
+
+ return child;
+ }
+
const mockChildProcess = () => {
jest.doMock('child_process', () => ({
+ spawn: fakeSpawn,
execFile: (command, args, options, callback) => {
+ // Legacy fallback for any code still using execFile
const cmd = String(command || '');
- const argv = Array.isArray(args) ? args.map(String) : [];
if (cmd === process.execPath || cmd === 'node') return callback(null, 'v22.0.0\n', '');
if (cmd === 'npm' || cmd === 'npm.cmd') return callback(null, '10.0.0\n', '');
if (cmd === 'git') return callback(null, 'git version 2.44.0\n', '');
- if (cmd === 'gh' && argv[0] === '--version') return callback(null, 'gh version 2.61.0\n', '');
- if (cmd === 'gh' && argv[0] === 'auth') {
- const err = new Error('not logged in');
- err.code = 1;
- return callback(err, '', 'not logged in');
- }
- if (cmd === 'claude') {
- const err = new Error('missing command: claude');
- err.code = 'ENOENT';
- return callback(err, '', '');
- }
- if (cmd === 'codex') {
- const err = new Error('missing command: codex');
- err.code = 'ENOENT';
- return callback(err, '', '');
- }
- if (cmd === 'bash' || cmd === 'bash.exe' || cmd === 'powershell.exe') {
- return callback(null, 'shell ok\n', '');
- }
+ if (cmd === 'gh') return callback(null, 'gh version 2.61.0\n', '');
+ if (cmd === 'bash' || cmd === 'bash.exe' || cmd === 'powershell.exe') return callback(null, 'shell ok\n', '');
const err = new Error(`missing command: ${cmd}`);
err.code = 'ENOENT';
return callback(err, '', '');
diff --git a/tests/unit/onboardingStateService.test.js b/tests/unit/onboardingStateService.test.js
new file mode 100644
index 00000000..1edbb30f
--- /dev/null
+++ b/tests/unit/onboardingStateService.test.js
@@ -0,0 +1,72 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const { OnboardingStateService } = require('../../server/onboardingStateService');
+
+describe('OnboardingStateService', () => {
+ const logger = { warn: jest.fn() };
+
+ beforeEach(() => {
+ logger.warn.mockReset();
+ });
+
+ test('returns default dependency setup state when no file exists', () => {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onboarding-state-default-'));
+ const storePath = path.join(tempDir, 'onboarding-state.json');
+ const service = new OnboardingStateService({ logger, storePath });
+
+ expect(service.getDependencySetupState()).toEqual({
+ completed: false,
+ dismissed: false,
+ currentStep: 0,
+ skippedActionIds: []
+ });
+ });
+
+ test('persists normalized dependency setup state across instances', () => {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onboarding-state-persist-'));
+ const storePath = path.join(tempDir, 'onboarding-state.json');
+ const service = new OnboardingStateService({ logger, storePath });
+
+ const updated = service.updateDependencySetupState({
+ completed: true,
+ dismissed: false,
+ currentStep: '4',
+ skippedActionIds: ['install-gh', 'install-gh', ' ', 'install-codex']
+ });
+
+ expect(updated).toEqual({
+ completed: true,
+ dismissed: false,
+ currentStep: 4,
+ skippedActionIds: ['install-gh', 'install-codex']
+ });
+
+ const reloaded = new OnboardingStateService({ logger, storePath });
+ expect(reloaded.getDependencySetupState()).toEqual(updated);
+ });
+
+ test('merges patches without dropping existing dependency setup state', () => {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onboarding-state-merge-'));
+ const storePath = path.join(tempDir, 'onboarding-state.json');
+ const service = new OnboardingStateService({ logger, storePath });
+
+ service.updateDependencySetupState({
+ completed: true,
+ currentStep: 3,
+ skippedActionIds: ['install-gh']
+ });
+
+ const updated = service.updateDependencySetupState({
+ dismissed: true
+ });
+
+ expect(updated).toEqual({
+ completed: true,
+ dismissed: true,
+ currentStep: 3,
+ skippedActionIds: ['install-gh']
+ });
+ });
+});