diff --git a/.nvmrc b/.nvmrc
index 25649a2b..a45fd52c 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-24.9.0
+24
diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md
index eadc9144..420974ad 100644
--- a/CODEBASE_DOCUMENTATION.md
+++ b/CODEBASE_DOCUMENTATION.md
@@ -56,6 +56,7 @@ server/notificationService.js - System notification manager
server/claudeVersionChecker.js - Claude Code version detection
server/tokenCounter.js - Token usage tracking (if applicable)
server/userSettingsService.js - User preferences and settings management
+├─ Desktop onboarding state: persists Windows/Tauri dependency-onboarding completion in `global.ui.onboarding.desktopDependencySetup`
server/sessionRecoveryService.js - Session recovery state persistence (CWD, agents, conversations)
├─ Recovery filtering: stale/non-configured session entries are pruned when requested by workspace-scoped APIs
├─ Agent clearing: `clearAgent()` resets stale `lastAgent` markers when a Claude/Codex terminal falls back to plain shell
@@ -538,5 +539,16 @@ LOGGING: Winston-based structured logging with rotation
9. **Mixed-repo workspaces**: Terminal naming must avoid conflicts between repos
10. **Template validation**: Always validate workspace templates against schemas
+
+## First-Run Dependency Onboarding (Windows)
+
+```
+server/setupActionService.js - Defines setup actions and launches PowerShell installers
+server/index.js - Routes: GET /api/setup-actions, POST /api/setup-actions/run
+client/app.js - Guided dependency onboarding steps + diagnostics integration
+client/index.html - Dependency onboarding modal markup + launch button
+client/styles.css - Dependency onboarding progress/step styling
+```
+
---
🚨 **END OF FILE - ENSURE YOU READ EVERYTHING ABOVE** 🚨
diff --git a/client/app.js b/client/app.js
index c9936ba9..a0253115 100644
--- a/client/app.js
+++ b/client/app.js
@@ -116,9 +116,156 @@ class ClaudeOrchestrator {
// navigate Prev/Next without reopening the Queue overlay.
this.reviewConsoleNav = null; // { source, createdAtMs, items: [{ id, kind, title, url, ... }], index }
+ this.currentTabId = null;
+ this.desktopLaunchTrace = {
+ id: this.createDesktopLaunchTraceId(),
+ enabled: this.shouldEnableDesktopLaunchTrace(),
+ seq: 0,
+ startedAt: new Date().toISOString()
+ };
+
this.init();
}
+ shouldEnableDesktopLaunchTrace() {
+ return this.isDesktopWindowsRuntime();
+ }
+
+ createDesktopLaunchTraceId() {
+ return `launch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
+ }
+
+ isWindowsHostEnvironment() {
+ try {
+ const platform = String(navigator?.platform || '').toLowerCase();
+ const userAgent = String(navigator?.userAgent || '').toLowerCase();
+ return platform.includes('win') || userAgent.includes('windows');
+ } catch {
+ return false;
+ }
+ }
+
+ hasDesktopLaunchToken() {
+ try {
+ const params = new URLSearchParams(window?.location?.search || '');
+ return !!String(params.get('token') || '').trim();
+ } catch {
+ return false;
+ }
+ }
+
+ isDesktopWindowsRuntime() {
+ return this.isWindowsHostEnvironment() && (this.hasDesktopLaunchToken() || !!window.__TAURI__);
+ }
+
+ sanitizeDesktopTraceValue(value, depth = 0, seen = new WeakSet()) {
+ if (value == null) return value;
+ if (typeof value === 'boolean' || typeof value === 'number') return value;
+ if (typeof value === 'string') {
+ return value.length > 500 ? `${value.slice(0, 484)}...[truncated]` : value;
+ }
+ if (typeof value === 'function') return `[function:${value.name || 'anonymous'}]`;
+ if (depth >= 4) return '[depth-limit]';
+ if (Array.isArray(value)) {
+ const items = value.slice(0, 15).map((item) => this.sanitizeDesktopTraceValue(item, depth + 1, seen));
+ if (value.length > 15) items.push(`[+${value.length - 15} more]`);
+ return items;
+ }
+ if (typeof value === 'object') {
+ if (seen.has(value)) return '[circular]';
+ seen.add(value);
+ const out = {};
+ const keys = Object.keys(value).slice(0, 20);
+ keys.forEach((key) => {
+ out[key] = this.sanitizeDesktopTraceValue(value[key], depth + 1, seen);
+ });
+ if (Object.keys(value).length > keys.length) {
+ out.__truncatedKeys = Object.keys(value).length - keys.length;
+ }
+ seen.delete(value);
+ return out;
+ }
+ return String(value);
+ }
+
+ getDesktopLaunchTraceContext() {
+ return {
+ appStartedAt: this.desktopLaunchTrace?.startedAt || null,
+ currentWorkspaceId: this.currentWorkspace?.id || null,
+ currentWorkspaceName: this.currentWorkspace?.name || null,
+ currentTabId: this.currentTabId || null,
+ isDashboardMode: !!this.isDashboardMode,
+ socketConnected: !!this.socket?.connected,
+ visibilityState: document?.visibilityState || null,
+ location: window?.location?.pathname || null
+ };
+ }
+
+ withLaunchTraceHeaders(init = {}) {
+ if (!this.desktopLaunchTrace?.enabled) {
+ return init || {};
+ }
+ return {
+ ...(init || {}),
+ headers: {
+ ...(init?.headers || {}),
+ 'x-launch-trace-id': this.desktopLaunchTrace.id
+ }
+ };
+ }
+
+ async traceDesktopLaunch(event, details = {}) {
+ if (!this.desktopLaunchTrace?.enabled) return false;
+
+ const safeDetails = this.sanitizeDesktopTraceValue(details);
+ const payload = {
+ traceId: this.desktopLaunchTrace.id,
+ seq: (this.desktopLaunchTrace.seq += 1),
+ event: String(event || '').trim() || 'client.event',
+ source: 'desktop-app',
+ details: (safeDetails && typeof safeDetails === 'object' && !Array.isArray(safeDetails))
+ ? { ...this.getDesktopLaunchTraceContext(), ...safeDetails }
+ : { ...this.getDesktopLaunchTraceContext(), value: safeDetails }
+ };
+
+ try {
+ await fetch('/api/desktop-launch-trace', this.withLaunchTraceHeaders({
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ keepalive: true
+ }));
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ emitWorkspaceSwitch(workspaceId, source = 'unknown', extra = {}) {
+ const id = String(workspaceId || '').trim();
+ if (!id || !this.socket) return;
+ if (this.isDesktopWindowsRuntime() && this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completed !== true) {
+ void this.traceDesktopLaunch('client.workspace-switch.blocked-onboarding', {
+ source,
+ workspaceId: id
+ });
+ this.showToast('Finish Orchestrator Setup before opening a workspace.', 'warning');
+ this.openDependencySetupWizard?.({ resetStep: false, source: 'workspace-switch-blocked' });
+ return;
+ }
+ void this.traceDesktopLaunch('client.workspace-switch.emitted', {
+ source,
+ workspaceId: id,
+ extra
+ });
+ this.socket.emit('switch-workspace', {
+ workspaceId: id,
+ source,
+ traceId: this.desktopLaunchTrace?.enabled ? this.desktopLaunchTrace.id : null,
+ ...extra
+ });
+ }
+
isMobileLayout() {
try {
return window.matchMedia && window.matchMedia('(max-width: 768px)').matches;
@@ -845,9 +992,16 @@ class ClaudeOrchestrator {
if (!expiresAt || expiresAt <= now) this.pendingWorktreeReservations.delete(key);
}
}
-
+
async init() {
try {
+ void this.traceDesktopLaunch('client.init.start', {
+ tauri: !!window.__TAURI__,
+ desktopRuntime: this.isDesktopWindowsRuntime(),
+ userAgent: String(navigator?.userAgent || ''),
+ platform: String(navigator?.platform || '')
+ });
+
// Initialize managers
this.terminalManager = new TerminalManager(this);
this.terminalManager.autosuggestEnabled = this.settings.autoSuggestions !== false;
@@ -984,23 +1138,25 @@ 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.syncDependencySetupWizardPreferences?.();
+ void this.bootstrapDependencySetupWizard?.();
this.applySidebarDesktopCollapsedFromPrefs();
this.refreshLicenseStatus?.().catch(() => {});
this.syncTerminalFiltersFromUserSettings();
@@ -1016,22 +1172,25 @@ 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);
+ void this.traceDesktopLaunch('client.init.failed', {
+ error: String(error?.message || error)
+ });
this.showError('Failed to initialize application');
}
}
-
+
async connectToServer() {
return new Promise((resolve, reject) => {
console.log('Attempting to connect to server...');
@@ -1044,32 +1203,47 @@ 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');
+ void this.traceDesktopLaunch('client.socket.connected', {
+ serverUrl,
+ socketId: this.socket?.id || null
+ });
this.updateConnectionStatus(true);
resolve();
});
-
+
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
+ void this.traceDesktopLaunch('client.socket.connect-error', {
+ serverUrl,
+ error: String(error?.message || 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');
+ void this.traceDesktopLaunch('client.socket.disconnected', {
+ socketId: this.socket?.id || null
+ });
this.updateConnectionStatus(false);
});
-
+
// Session events
this.socket.on('sessions', async (sessionStates) => {
console.log('Received sessions event:', sessionStates);
+ void this.traceDesktopLaunch('client.sessions.received', {
+ sessionCount: Object.keys(sessionStates || {}).length,
+ sessionIds: Object.keys(sessionStates || {})
+ });
// Pre-fetch worktree configs if we have an active workspace
if (this.currentWorkspace) {
@@ -1085,7 +1259,7 @@ class ClaudeOrchestrator {
this.worktreeTags.set(worktreePath, tag || {});
this.buildSidebar();
});
-
+
this.socket.on('terminal-output', ({ sessionId, data }) => {
this.terminalManager.handleOutput(sessionId, data);
@@ -1133,7 +1307,7 @@ class ClaudeOrchestrator {
this.sessionActivity.set(sessionId, 'active');
}
});
-
+
this.socket.on('autosuggest-response', ({ sessionId, suggestion, prefix }) => {
this.terminalManager.handleAutosuggestResponse(sessionId, suggestion, prefix);
});
@@ -1142,15 +1316,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 +1517,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,19 +1535,30 @@ 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);
+ void this.traceDesktopLaunch('client.user-settings.updated', {
+ onboardingCompleted: settings?.global?.ui?.onboarding?.desktopDependencySetup?.completed === true,
+ onboardingCompletedAt: settings?.global?.ui?.onboarding?.desktopDependencySetup?.completedAt || null
+ });
this.userSettings = settings;
this.syncUserSettingsUI();
this.applyThemeFromUserSettings();
this.applySimpleModeConfig();
this.maybeAutoOpenSimpleMode();
+ this.syncDependencySetupWizardPreferences?.();
});
// Workspace events
this.socket.on('workspace-info', async ({ active, available, config, workspaceTypes, frameworks, cascadedConfigs }) => {
console.log('Received workspace info:', { active, available, config, workspaceTypes, frameworks, cascadedConfigs });
+ void this.traceDesktopLaunch('client.workspace-info.received', {
+ activeWorkspaceId: active?.id || null,
+ activeWorkspaceName: active?.name || null,
+ availableWorkspaceCount: Array.isArray(available) ? available.length : 0,
+ startupDashboard: config?.ui?.startupDashboard === true
+ });
this.currentWorkspace = active;
this.availableWorkspaces = available;
this.orchestratorConfig = config;
@@ -1449,6 +1634,11 @@ class ClaudeOrchestrator {
this.socket.on('workspace-changed', async ({ workspace, sessions }) => {
console.log('Workspace changed:', workspace.name);
+ void this.traceDesktopLaunch('client.workspace-changed.received', {
+ workspaceId: workspace?.id || null,
+ workspaceName: workspace?.name || null,
+ sessionCount: Object.keys(sessions || {}).length
+ });
// If tab manager is enabled, create a new tab for this workspace
if (this.tabManager) {
@@ -1589,7 +1779,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 +1790,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 +1847,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 +1866,7 @@ class ClaudeOrchestrator {
}
}, 30000);
}
-
+
setupEventListeners() {
// Check if elements exist before adding listeners
const elements = {
@@ -1739,7 +1929,7 @@ class ClaudeOrchestrator {
'start-claude',
'cancel-claude-startup'
]);
-
+
// Check all elements exist
for (const id in elements) {
elements[id] = document.getElementById(id);
@@ -1747,7 +1937,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 +2046,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 +2068,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 +2096,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 +2126,7 @@ class ClaudeOrchestrator {
} else {
console.error('Settings toggle button not found!');
}
-
+
document.getElementById('close-settings').addEventListener('click', () => {
closeSettingsPanel();
});
@@ -1962,7 +2152,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 +2161,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 +2184,7 @@ class ClaudeOrchestrator {
}
}
});
-
+
document.getElementById('theme-select').addEventListener('change', (e) => {
this.settings.theme = e.target.value;
this.saveSettings();
@@ -2048,6 +2238,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 +2497,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 +2918,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 +3090,7 @@ class ClaudeOrchestrator {
});
});
}
-
+
setViewMode(mode, { persist = true } = {}) {
const normalized = String(mode || '').toLowerCase();
if (!['all', 'claude', 'server'].includes(normalized)) return;
@@ -2914,7 +3105,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 +3735,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,9 +3762,14 @@ class ClaudeOrchestrator {
&& this.matchesTierFilter(sessionId)
&& this.matchesWorkflowMode(sessionId);
}
-
+
handleInitialSessions(sessionStates) {
console.log('Received initial sessions:', sessionStates);
+ void this.traceDesktopLaunch('client.sessions.rendered', {
+ sessionCount: Object.keys(sessionStates || {}).length,
+ sessionIds: Object.keys(sessionStates || {}),
+ workspaceId: this.currentWorkspace?.id || null
+ });
// Preserve per-workspace worktree visibility (hide/show toggles) when we
// receive a sessions refresh for the SAME workspace (e.g. after adding a
@@ -3588,7 +3784,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 +3830,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 +4433,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 +4470,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 +4604,9 @@ class ClaudeOrchestrator {
`;
-
+
// Click handler is already attached via event delegation in setupEventListeners
-
+
worktreeList.appendChild(item);
}
@@ -4791,21 +4987,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 +5034,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 +5063,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 +5075,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 +5090,7 @@ class ClaudeOrchestrator {
this.visibleTerminals.add(sessionId);
}
}
-
+
// If no active sessions, show all
if (this.visibleTerminals.size === 0) {
this.showAllTerminals();
@@ -4903,7 +5099,7 @@ class ClaudeOrchestrator {
this.buildSidebar();
}
}
-
+
resizeAllVisibleTerminals() {
// Force resize all visible terminals to fit their containers
this.activeView.forEach(sessionId => {
@@ -5037,7 +5233,7 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
showWorktree(worktreeIdOrKey) {
// Show terminals for this EXACT worktree key
const claudeId = `${worktreeIdOrKey}-claude`;
@@ -5049,17 +5245,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 +5284,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 +5428,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 +5473,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 +5487,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 +5520,7 @@ class ClaudeOrchestrator {
grid.appendChild(wrapper);
}
});
-
+
// Now handle terminal instances
sortedSessionIds.forEach((sessionId, index) => {
const session = this.sessions.get(sessionId);
@@ -5332,25 +5528,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 +5772,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 +5816,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 +5854,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 +5867,7 @@ class ClaudeOrchestrator {
el.title = tooltip;
return;
}
-
+
let el = existing;
if (!isSpan) {
existing?.remove();
@@ -5687,7 +5883,7 @@ class ClaudeOrchestrator {
el.textContent = label;
el.title = tooltip;
}
-
+
refreshBranchLabels() {
try {
for (const [sessionId, session] of this.sessions) {
@@ -5699,7 +5895,7 @@ class ClaudeOrchestrator {
}
this.buildSidebar();
}
-
+
createTerminalElement(sessionId, session) {
const wrapper = document.createElement('div');
wrapper.className = 'terminal-wrapper';
@@ -5709,7 +5905,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 +6015,7 @@ class ClaudeOrchestrator {
return wrapper;
}
-
+
updateSessionStatus(sessionId, status) {
const statusElement = document.getElementById(this.getSessionDomId('status', sessionId));
// Update session data
@@ -5903,7 +6099,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 +6131,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 +6192,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR) {
const session = this.sessions.get(sessionId);
if (session) {
@@ -6007,9 +6203,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 +6215,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 +6252,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 +6285,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 +6311,7 @@ class ClaudeOrchestrator {
}
});
}
-
+
buildProduction(sessionId) {
// Extract worktree number from sessionId (e.g., 'work1-claude' -> 1)
const worktreeMatch = sessionId.match(/work(\d+)/);
@@ -6124,10 +6320,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 +6332,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 +6360,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 +6369,7 @@ class ClaudeOrchestrator {
if (secondUrlIndex > 0) {
url = url.slice(0, secondUrlIndex);
}
-
+
// Validate URL format
try {
new URL(url);
@@ -6181,7 +6377,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 +6397,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 +6438,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 +6457,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 +6469,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
// Show PR button if PR link detected
if (links.pr) {
const lastLogged = this.githubLinkLogs.get(sessionId);
@@ -6288,17 +6484,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 +6574,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 +6681,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 +6714,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 +6743,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 +7718,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
handleSessionRestart(sessionId) {
console.log(`Session ${sessionId} restarted`);
// Terminal will automatically reconnect and show new content
@@ -7551,20 +7747,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 +7768,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 +7779,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 +7797,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 +7817,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 +7857,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 = `
-
@@ -8767,19 +8945,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 +8970,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 +8979,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 +9029,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 +9037,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 +9058,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 +9078,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 +9098,9 @@ class ClaudeOrchestrator {
});
request += `\n`;
}
-
+
request += `Please provide a thorough code review with specific feedback and suggestions.\n`;
-
+
return request;
}
@@ -9267,58 +9445,1541 @@ class ClaudeOrchestrator {
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');
+ 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 = this.isWindowsHostEnvironment();
+ const isDesktopWindowsApp = this.isDesktopWindowsRuntime();
+ let desktopCompleted = false;
+ const traceOnboarding = (event, details = {}) => {
+ void this.traceDesktopLaunch(`client.onboarding.${event}`, details);
+ };
+
+ const setBootstrapPending = (pending) => {
+ if (!isWindowsHost) return;
+ if (pending) {
+ body?.classList?.add?.('dependency-onboarding-booting');
+ body?.classList?.remove?.('dependency-onboarding-active');
+ return;
+ }
+ body?.classList?.remove?.('dependency-onboarding-booting');
+ };
+ if (!isWindowsHost) {
+ setBootstrapPending(false);
+ return;
+ }
+ setBootstrapPending(false);
+
+ const dismissKey = 'orchestrator-dependency-setup-dismissed-v3';
+ const completedKey = 'orchestrator-dependency-onboarding-completed-v2';
+ const progressKey = 'orchestrator-dependency-onboarding-progress-v2';
+ const skippedStepsKey = 'orchestrator-dependency-onboarding-skipped-v1';
+ const state = {
+ loading: false,
+ diagnostics: null,
+ actions: [],
+ currentStep: 0,
+ showWelcome: true,
+ skippedActionIds: new Set(),
+ actionRuns: new Map(),
+ actionRunPollers: new Map(),
+ gitIdentity: {
+ name: '',
+ email: ''
+ },
+ gitIdentityHelpVisible: false
+ };
+
+ const syncDesktopCompleted = () => {
+ if (!isDesktopWindowsApp) return false;
+ desktopCompleted = !!this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completed;
+ return desktopCompleted;
+ };
+ syncDesktopCompleted();
+ traceOnboarding('wizard-ready', {
+ isWindowsHost,
+ isDesktopWindowsApp,
+ desktopCompleted
+ });
+
+ const readDismissed = () => {
+ try {
+ return localStorage.getItem(dismissKey) === 'true';
+ } catch {
+ return false;
+ }
+ };
+
+ const writeDismissed = (value) => {
+ try {
+ if (value) localStorage.setItem(dismissKey, 'true');
+ else localStorage.removeItem(dismissKey);
+ } catch {
+ // ignore
+ }
+ };
+
+ const readCompleted = () => {
+ if (isDesktopWindowsApp) {
+ return syncDesktopCompleted();
+ }
+ try {
+ return localStorage.getItem(completedKey) === 'true';
+ } catch {
+ return false;
+ }
+ };
+
+ const writeCompleted = async (value) => {
+ if (isDesktopWindowsApp) {
+ const next = !!value;
+ desktopCompleted = next;
+ traceOnboarding('completion-write-start', {
+ nextCompleted: next
+ });
+ if (this.userSettings) {
+ await this.updateGlobalUserSetting('ui.onboarding.desktopDependencySetup', {
+ completed: next,
+ completedAt: next ? new Date().toISOString() : null
+ });
+ traceOnboarding('completion-write-success', {
+ nextCompleted: next,
+ persistedCompleted: !!this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completed,
+ persistedCompletedAt: this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completedAt || null
+ });
+ } else {
+ traceOnboarding('completion-write-skipped', {
+ nextCompleted: next,
+ reason: 'user-settings-missing'
+ });
+ }
+ return;
+ }
+ try {
+ if (value) localStorage.setItem(completedKey, 'true');
+ else localStorage.removeItem(completedKey);
+ } catch {
+ // ignore
+ }
+ };
+
+ const readSavedStep = () => {
+ try {
+ const raw = Number.parseInt(String(localStorage.getItem(progressKey) || ''), 10);
+ if (Number.isFinite(raw) && raw >= 0) return raw;
+ return 0;
+ } catch {
+ return 0;
+ }
+ };
+
+ const writeSavedStep = (step) => {
+ try {
+ localStorage.setItem(progressKey, String(Math.max(0, Number(step) || 0)));
+ } catch {
+ // ignore
+ }
+ };
+
+ const readSkippedStepIds = () => {
+ try {
+ const raw = localStorage.getItem(skippedStepsKey);
+ if (!raw) return new Set();
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return new Set();
+ const ids = parsed
+ .map((value) => String(value || '').trim())
+ .filter(Boolean);
+ return new Set(ids);
+ } catch {
+ return new Set();
+ }
+ };
+
+ const writeSkippedStepIds = () => {
+ try {
+ if (!(state.skippedActionIds instanceof Set) || state.skippedActionIds.size === 0) {
+ localStorage.removeItem(skippedStepsKey);
+ return;
+ }
+ localStorage.setItem(skippedStepsKey, JSON.stringify(Array.from(state.skippedActionIds)));
+ } catch {
+ // ignore
+ }
+ };
+
+ const setStepSkipped = (actionId, skipped) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ if (skipped) state.skippedActionIds.add(id);
+ else state.skippedActionIds.delete(id);
+ writeSkippedStepIds();
+ };
+
+ const toToolMap = (diagnostics) => {
+ const map = new Map();
+ const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : [];
+ tools.forEach((tool) => {
+ const id = String(tool?.id || '').trim();
+ if (!id) return;
+ map.set(id, !!tool?.ok);
+ });
+ return map;
+ };
+
+ const getToolResult = (diagnostics, toolId) => {
+ const id = String(toolId || '').trim();
+ if (!id) return null;
+ const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : [];
+ return tools.find((tool) => String(tool?.id || '').trim() === id) || null;
+ };
+
+ const parseGitIdentityVersion = (value) => {
+ const raw = String(value || '').trim();
+ if (!raw) return { name: '', email: '' };
+ const pair = raw.match(/^(.*)\s<([^<>]+)>$/);
+ if (pair?.[1] && pair?.[2]) {
+ return {
+ name: String(pair[1] || '').trim(),
+ email: String(pair[2] || '').trim()
+ };
+ }
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) {
+ return { name: '', email: raw };
+ }
+ return { name: raw, email: '' };
+ };
+
+ const stripAnsiText = (value) => String(value || '').replace(/\u001b\[[0-9;]*m/g, '');
+
+ const collectRunOutputLines = (runInfo, { limit = 25 } = {}) => {
+ const lines = Array.isArray(runInfo?.output)
+ ? runInfo.output
+ .map((entry) => stripAnsiText(String(entry?.line || '')).trim())
+ .filter(Boolean)
+ : [];
+ if (!Number.isFinite(limit) || limit <= 0) return lines;
+ return lines.slice(-Math.max(1, Number(limit) || 1));
+ };
+
+ const extractGithubLoginInfo = (lines = []) => {
+ const fallbackUrl = 'https://github.com/login/device';
+ let link = fallbackUrl;
+ let code = '';
+ let sawDeviceHint = false;
+
+ (Array.isArray(lines) ? lines : []).forEach((lineRaw) => {
+ const line = String(lineRaw || '').trim();
+ if (!line) return;
+
+ if (/one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i.test(line)) {
+ sawDeviceHint = true;
+ }
+
+ const linkMatch = line.match(/https:\/\/github\.com\/login\/device(?:\S*)?/i);
+ if (linkMatch?.[0]) link = linkMatch[0].trim();
+
+ const codeMatch = line.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i);
+ if (codeMatch?.[1]) code = String(codeMatch[1]).toUpperCase();
+ });
+
+ return {
+ link,
+ code,
+ sawDeviceHint
+ };
+ };
+
+ const hydrateGitIdentityDraft = (diagnostics) => {
+ const gitIdentityTool = getToolResult(diagnostics, 'gitIdentity');
+ const parsed = parseGitIdentityVersion(String(gitIdentityTool?.version || ''));
+ if (!state.gitIdentity.name && parsed.name) {
+ state.gitIdentity.name = parsed.name;
+ }
+ if (!state.gitIdentity.email && parsed.email) {
+ state.gitIdentity.email = parsed.email;
+ }
+ };
+
+ const getRequirementState = (toolsMap) => {
+ const gitOk = !!toolsMap.get('git');
+ const claudeOk = !!toolsMap.get('claude');
+ const codexOk = !!toolsMap.get('codex');
+ const hasAgentCli = claudeOk || codexOk;
+ const coreReady = gitOk && hasAgentCli;
+ const missingCore = [];
+ if (!gitOk) missingCore.push('git');
+ if (!hasAgentCli) missingCore.push('agent-cli');
+ return {
+ gitOk,
+ claudeOk,
+ codexOk,
+ hasAgentCli,
+ coreReady,
+ missingCore
+ };
+ };
+
+ const isActionComplete = (actionId, toolsMap) => {
+ switch (String(actionId || '').trim()) {
+ case 'install-git':
+ return !!toolsMap.get('git');
+ case 'configure-git-identity':
+ return !!toolsMap.get('gitIdentity');
+ case 'install-node':
+ return !!toolsMap.get('node') && !!toolsMap.get('npm');
+ case 'install-gh':
+ return !!toolsMap.get('gh');
+ case 'gh-login':
+ return !!toolsMap.get('ghAuth');
+ case 'install-claude':
+ return !!toolsMap.get('claude');
+ case 'install-codex':
+ return !!toolsMap.get('codex');
+ default:
+ return false;
+ }
+ };
+
+ const getActionLevelText = (level) => {
+ if (level === 'required') return 'Required';
+ if (level === 'optional') return 'Optional';
+ if (level === 'core-option') return 'Core option';
+ return 'Recommended';
+ };
+
+ const getActionLevelClass = (level) => {
+ if (level === 'optional') return 'level-optional';
+ return level === 'recommended' ? 'level-recommended' : 'level-required';
+ };
+
+ const getActionStatusText = (actionId, done) => {
+ const id = String(actionId || '').trim();
+ if (id === 'gh-login') return done ? 'Logged in' : 'Not logged in';
+ if (id === 'configure-git-identity') return done ? 'Configured' : 'Not configured';
+ return done ? 'Installed' : 'Missing';
+ };
+
+ const getResolvedSteps = () => {
+ const toolsMap = toToolMap(state.diagnostics);
+ const actions = Array.isArray(state.actions) ? state.actions : [];
+ return actions.map((action) => {
+ const id = String(action?.id || '').trim();
+ const level = getActionLevel(id);
+ const done = isActionComplete(id, toolsMap);
+ return {
+ ...action,
+ id,
+ level,
+ optional: action?.optional === true || level === 'optional',
+ done,
+ levelText: getActionLevelText(level),
+ levelClass: getActionLevelClass(level),
+ statusText: getActionStatusText(id, done),
+ statusClass: done ? 'status-ok' : 'status-missing',
+ runSupported: action?.runSupported !== false
+ };
+ });
+ };
+
+ const syncSkippedSteps = (steps) => {
+ if (!(state.skippedActionIds instanceof Set)) {
+ state.skippedActionIds = new Set();
+ }
+ const validSkippedIds = new Set(
+ (Array.isArray(steps) ? steps : [])
+ .filter((step) => {
+ const id = String(step?.id || '').trim();
+ return !!id && step?.optional && !step?.done;
+ })
+ .map((step) => String(step?.id || '').trim())
+ );
+ let changed = false;
+ for (const id of Array.from(state.skippedActionIds)) {
+ if (!validSkippedIds.has(id)) {
+ state.skippedActionIds.delete(id);
+ changed = true;
+ }
+ }
+ if (changed) writeSkippedStepIds();
+ };
+
+ const isOnboardingLocked = () => {
+ if (!isWindowsHost) return false;
+ if (!Array.isArray(state.actions) || state.actions.length === 0) return false;
+ const toolsMap = toToolMap(state.diagnostics);
+ const req = getRequirementState(toolsMap);
+ if (!req?.coreReady) return true;
+ return !readCompleted();
+ };
+
+ const applyOnboardingLockUI = () => {
+ const locked = isOnboardingLocked();
+ if (closeBtn) {
+ closeBtn.disabled = locked;
+ closeBtn.style.visibility = locked ? 'hidden' : '';
+ }
+ modal.setAttribute('data-onboarding-locked', locked ? 'true' : 'false');
+ return locked;
+ };
+
+ const setCurrentStep = (nextStep, { persist = true } = {}) => {
+ const previousStep = state.currentStep;
+ const maxStep = Math.max(0, (Array.isArray(state.actions) ? state.actions.length : 0) - 1);
+ const parsed = Number.parseInt(String(nextStep), 10);
+ const safe = Number.isFinite(parsed) ? parsed : 0;
+ state.currentStep = Math.max(0, Math.min(safe, maxStep));
+ if (state.currentStep !== previousStep) {
+ state.gitIdentityHelpVisible = false;
+ }
+ if (persist) writeSavedStep(state.currentStep);
+ return state.currentStep;
+ };
+
+ const getActionLevel = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (id === 'install-git') return 'required';
+ if (id === 'configure-git-identity') return 'optional';
+ if (id === 'install-gh' || id === 'gh-login') return 'optional';
+ if (id === 'install-claude') return 'optional';
+ if (id === 'install-codex') return 'optional';
+ return 'recommended';
+ };
+
+ const buildStepIconSvg = (iconMarkup) => (
+ `
`
+ );
+
+ const stepIconSvgByActionId = Object.freeze({
+ 'install-git': buildStepIconSvg('
'),
+ 'configure-git-identity': buildStepIconSvg('
'),
+ 'install-node': buildStepIconSvg('
'),
+ 'install-gh': buildStepIconSvg('
'),
+ 'gh-login': buildStepIconSvg('
'),
+ 'install-claude': buildStepIconSvg('
'),
+ 'install-codex': buildStepIconSvg('
')
+ });
+
+ const getStepIconSvg = (actionId) => {
+ const id = String(actionId || '').trim();
+ return stepIconSvgByActionId[id]
+ || buildStepIconSvg('
');
+ };
+
+ const render = () => {
+ const toolsMap = toToolMap(state.diagnostics);
+ const req = getRequirementState(toolsMap);
+ const steps = getResolvedSteps();
+ syncSkippedSteps(steps);
+ if (!steps.length) {
+ summaryEl.textContent = 'No setup actions are available for this platform.';
+ listEl.innerHTML = '
No setup actions are available for this platform.
';
+ return { req, steps, current: null };
+ }
+
+ setCurrentStep(state.currentStep, { persist: false });
+ const current = steps[state.currentStep];
+ const stepNo = state.currentStep + 1;
+ const totalSteps = steps.length;
+ const detectedCount = steps.filter((step) => step.done).length;
+ const doneRatio = totalSteps > 0 ? Math.round((detectedCount / totalSteps) * 100) : 0;
+ const missingCore = [];
+ if (!req.gitOk) missingCore.push('Git');
+ if (!req.hasAgentCli) missingCore.push('Claude Code or Codex CLI');
+
+ if (state.showWelcome) {
+ summaryEl.textContent = '';
+ listEl.innerHTML = `
+
+
Let’s get you ready in a minute.
+
+ We’ll check your system and install what’s needed.
+ Optional tools can be skipped.
+
+
+
+
+
`;
+ return { req, steps, current };
+ }
+
+ summaryEl.textContent = '';
+
+ const currentId = String(current?.id || '').trim();
+ const currentStepIconSvg = getStepIconSvg(currentId);
+ const currentTitle = this.escapeHtml(String(current?.title || currentId || 'Setup action'));
+ const currentDesc = this.escapeHtml(String(current?.description || ''));
+ const commandRaw = String(current?.command || '');
+ const runInfo = state.actionRuns.get(currentId) || null;
+ const runStatus = String(runInfo?.status || '').trim().toLowerCase();
+ const isRunning = runStatus === 'running';
+ const isVerifying = runStatus === 'verifying';
+ const isFinalizing = runStatus === 'success' || runStatus === 'completed';
+ const isRunBusy = isRunning || isVerifying || isFinalizing;
+ const isGitIdentityStep = currentId === 'configure-git-identity';
+ const runOutputAll = collectRunOutputLines(runInfo, { limit: 160 });
+ const runOutput = runOutputAll.slice(-8);
+ const runOutputText = this.escapeHtml(runOutput.join('\n'));
+ const shouldShowInstallerOutput = currentId !== 'gh-login' && !isGitIdentityStep && (
+ runOutput.length > 0 ||
+ isRunBusy ||
+ runStatus === 'failed' ||
+ runStatus === 'needs-attention'
+ );
+ const installerOutputText = runOutput.length
+ ? runOutputText
+ : this.escapeHtml(
+ isRunning
+ ? 'Installer started. Waiting for output...'
+ : (isVerifying
+ ? 'Installer finished. Verifying dependency...'
+ : 'No installer output captured yet.')
+ );
+ const githubDeviceUrl = 'https://github.com/login/device';
+ const ghInstalled = !!toolsMap.get('gh');
+ const ghLoggedIn = !!toolsMap.get('ghAuth');
+ const ghLoginRunInfo = state.actionRuns.get('gh-login') || null;
+ const ghLoginRunStatus = String(ghLoginRunInfo?.status || '').trim().toLowerCase();
+ const ghLoginIsRunning = ghLoginRunStatus === 'running';
+ const ghLoginIsVerifying = ghLoginRunStatus === 'verifying';
+ const ghLoginIsFinalizing = ghLoginRunStatus === 'success' || ghLoginRunStatus === 'completed';
+ const ghLoginIsBusy = ghLoginIsRunning || ghLoginIsVerifying || ghLoginIsFinalizing;
+ const ghLoginOutputAll = collectRunOutputLines(ghLoginRunInfo, { limit: 160 });
+ const ghLoginInfo = extractGithubLoginInfo(ghLoginOutputAll);
+ const ghLoginLink = String(ghLoginRunInfo?.ghDeviceUrl || ghLoginInfo.link || githubDeviceUrl).trim() || githubDeviceUrl;
+ const ghLoginCode = String(ghLoginRunInfo?.ghDeviceCode || ghLoginInfo.code || '').trim().toUpperCase();
+ const ghLoginHasSignal = !!(
+ ghLoginRunInfo?.ghHasDeviceHint
+ || ghLoginInfo.sawDeviceHint
+ || ghLoginCode
+ || ghLoginLink !== githubDeviceUrl
+ );
+ const ghLoginUiPhase = (() => {
+ if (!ghInstalled || ghLoggedIn) return 'none';
+ if (!ghLoginRunInfo) return 'start';
+ if (ghLoginCode) return 'code';
+ if (ghLoginIsBusy) return 'wait-code';
+ return 'retry';
+ })();
+ const ghLoginInlineStatusText = (() => {
+ if (!ghInstalled) return 'Install GitHub CLI first';
+ if (ghLoggedIn) return 'Logged in';
+ if (ghLoginIsFinalizing) return 'Finalizing login';
+ if (ghLoginIsRunning) return 'Signing in';
+ if (ghLoginIsVerifying) return 'Checking login';
+ return 'Not logged in';
+ })();
+ const ghLoginInlineStatusClass = ghLoggedIn
+ ? 'status-ok'
+ : ((ghLoginIsBusy || ghLoginRunStatus === 'needs-attention') ? 'status-pending' : 'status-missing');
+ const ghLoginInlineRunLabel = (() => {
+ if (ghLoggedIn) return 'Logged in';
+ if (ghLoginIsFinalizing) return 'Finalizing...';
+ if (ghLoginIsBusy) return 'Waiting...';
+ return 'Start login';
+ })();
+ const ghLoginInlineRunDisabled = !ghInstalled || ghLoggedIn || ghLoginIsBusy;
+ const showInlineGhLogin = currentId === 'install-gh' && ghInstalled;
+ const isGhLoginStep = currentId === 'gh-login';
+ const codexNeedsNode = currentId === 'install-codex' && !(toolsMap.get('node') && toolsMap.get('npm'));
+ const gitIdentityName = this.escapeHtml(String(state.gitIdentity?.name || ''));
+ const gitIdentityEmail = this.escapeHtml(String(state.gitIdentity?.email || ''));
+ const showRunButton = current?.runSupported !== false && !isGitIdentityStep && !(isGhLoginStep && current?.done);
+ const runDisabled = !!current?.done || runStatus === 'verified' || isRunBusy || codexNeedsNode;
+ const runLabel = (() => {
+ if (current?.done || runStatus === 'verified') {
+ if (currentId === 'gh-login') return 'Logged in';
+ if (currentId === 'configure-git-identity') return 'Configured';
+ return 'Installed';
+ }
+ if (isFinalizing) return 'Finalizing...';
+ if (isRunBusy) return isGhLoginStep ? 'Waiting...' : 'Running...';
+ if (currentId === 'gh-login') return 'Start login';
+ return 'Run step';
+ })();
+ const baseStatusText = String(current?.statusText || (current?.done ? 'Installed' : 'Missing'));
+ const statusText = (() => {
+ if (runStatus === 'verified') return baseStatusText;
+ if (isFinalizing) return isGhLoginStep ? 'Finalizing login' : 'Finalizing';
+ if (isRunning) return isGhLoginStep ? 'Signing in' : (isGitIdentityStep ? 'Saving' : 'Installing');
+ if (isVerifying) return isGhLoginStep ? 'Checking login' : (isGitIdentityStep ? 'Checking' : 'Verifying');
+ if (runStatus === 'failed') return isGhLoginStep ? 'Login failed' : (isGitIdentityStep ? 'Save failed' : 'Failed');
+ return baseStatusText;
+ })();
+ const statusClass = current?.done || runStatus === 'verified'
+ ? 'status-ok'
+ : ((isRunning || isVerifying || isFinalizing) ? 'status-pending' : (runStatus === 'failed' ? 'status-missing' : (current?.statusClass || 'status-missing')));
+ let guidance = 'Run this step. We will detect completion automatically.';
+ if (current?.done || runStatus === 'verified') {
+ guidance = isGhLoginStep
+ ? 'GitHub CLI is authenticated. Continue to the next step.'
+ : (currentId === 'install-gh'
+ ? 'GitHub CLI is installed. Optional next step: sign in with GitHub to enable PR and repo actions.'
+ : (isGitIdentityStep
+ ? 'Git identity is configured. Continue to the next step.'
+ : 'Already installed on this machine. Continue to the next step.'));
+ } else if (isRunning) {
+ guidance = isGhLoginStep
+ ? 'GitHub login started. Use the browser flow below and keep this window open; we recheck automatically.'
+ : (isGitIdentityStep
+ ? 'Saving Git identity now. Keep this window open and we will recheck automatically.'
+ : 'Installing now via PowerShell. Keep this window open and we will recheck automatically.');
+ } else if (isVerifying) {
+ guidance = isGhLoginStep
+ ? 'Checking GitHub login automatically...'
+ : (isGitIdentityStep
+ ? 'Git identity saved. Checking your system automatically...'
+ : 'Install command finished. Checking your system automatically...');
+ } else if (isFinalizing) {
+ guidance = isGhLoginStep
+ ? 'Login command finished. Finalizing and checking status automatically...'
+ : 'Install command finished. Finalizing and checking your system automatically...';
+ } else if (runStatus === 'failed') {
+ const errorText = String(runInfo?.error || '').trim();
+ guidance = errorText
+ ? `${isGhLoginStep ? 'Login failed' : (isGitIdentityStep ? 'Save failed' : 'Install failed')}: ${errorText}`
+ : `${isGhLoginStep ? 'Login failed' : (isGitIdentityStep ? 'Save failed' : 'Install failed')}. Review and run the step again.`;
+ } else if (runStatus === 'needs-attention') {
+ guidance = isGhLoginStep
+ ? (ghLoginHasSignal
+ ? 'GitHub login is not detected yet. If browser sign-in is complete, click Start login again to request a new code.'
+ : 'GitHub CLI did not return a one-time code. Click Start login again; if needed, reinstall GitHub CLI first.')
+ : (isGitIdentityStep
+ ? 'Git identity was saved, but it is still not detected. Check your values and save again.'
+ : 'Install command finished, but this dependency is still not detected. Review output below and run again.');
+ } else if (isGitIdentityStep) {
+ guidance = 'Enter your Git name and email, then click Save identity. We will detect it automatically.';
+ } else if (isGhLoginStep) {
+ guidance = 'Optional after GitHub CLI install. Start login when you are ready.';
+ } else if (codexNeedsNode) {
+ guidance = 'Install Node.js LTS first. Codex uses npm and cannot be installed until Node is detected.';
+ } else if (!current?.runSupported && current?.optional) {
+ guidance = 'Optional but strongly recommended: set Git user.name and user.email so commits and PR authorship are correct.';
+ } else if (!current?.runSupported) {
+ guidance = 'Manual step: run the command below in your terminal. We will detect it automatically afterward.';
+ }
+ if (currentId === 'install-gh' && (current?.done || runStatus === 'verified')) {
+ if (ghLoggedIn) {
+ guidance = 'GitHub CLI is installed and authenticated. You can continue.';
+ } else if (ghLoginIsRunning) {
+ guidance = 'GitHub login started. Use the browser flow in this step and keep this window open.';
+ } else if (ghLoginIsVerifying) {
+ guidance = 'Checking GitHub login automatically...';
+ } else if (ghLoginIsFinalizing) {
+ guidance = 'Login command finished. Finalizing and checking status automatically...';
+ } else if (ghLoginRunStatus === 'needs-attention') {
+ guidance = ghLoginHasSignal
+ ? 'GitHub login is not detected yet. If browser sign-in is complete, click Start login again to request a new code.'
+ : 'GitHub CLI did not return a one-time code. Click Start login again.';
+ }
+ }
+ 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();
+ traceOnboarding('modal-close-requested', {
+ force,
+ locked
+ });
+ if (!force && locked) {
+ openModal();
+ return false;
+ }
+ modal.classList.add('hidden');
+ body?.classList?.remove?.('dependency-onboarding-active');
+ setBootstrapPending(false);
+ return true;
+ };
+ const openModal = ({ showWelcome = null } = {}) => {
+ const wasHidden = modal.classList.contains('hidden');
+ traceOnboarding('modal-opened', {
+ wasHidden,
+ requestedShowWelcome: typeof showWelcome === 'boolean' ? showWelcome : null
+ });
+ modal.classList.remove('hidden');
+ setBootstrapPending(false);
+ body?.classList?.add?.('dependency-onboarding-active');
+ if (typeof showWelcome === 'boolean') {
+ state.showWelcome = showWelcome;
+ } else if (wasHidden) {
+ state.showWelcome = true;
+ }
+ if (state.diagnostics && Array.isArray(state.actions) && state.actions.length > 0) {
+ render();
+ }
+ applyOnboardingLockUI();
+ };
+
+ const setLoading = (loading) => {
+ state.loading = !!loading;
+ if (openBtn) openBtn.disabled = state.loading;
+ if (state.loading) {
+ summaryEl.textContent = '';
+ }
+ };
+
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
+
+ const loadAndRender = async ({ open = false, forceAutoShow = false, bootstrap = false, explicitOpen = false } = {}) => {
+ if (state.loading) return false;
+ traceOnboarding('load-start', {
+ open,
+ forceAutoShow,
+ bootstrap,
+ explicitOpen
+ });
+ setLoading(true);
+ try {
+ const [diagRes, actionsRes] = await Promise.all([
+ fetch('/api/diagnostics', this.withLaunchTraceHeaders()),
+ fetch('/api/setup-actions', this.withLaunchTraceHeaders())
+ ]);
+ const diagData = await diagRes.json().catch(() => ({}));
+ const actionsData = await actionsRes.json().catch(() => ({}));
+
+ if (!diagRes.ok || diagData?.ok === false) {
+ throw new Error(String(diagData?.error || `Diagnostics HTTP ${diagRes.status}`));
+ }
+ if (!actionsRes.ok || actionsData?.ok === false) {
+ throw new Error(String(actionsData?.error || `Setup actions HTTP ${actionsRes.status}`));
+ }
+
+ state.diagnostics = diagData;
+ hydrateGitIdentityDraft(diagData);
+ const allActions = Array.isArray(actionsData?.actions) ? actionsData.actions : [];
+ const toolsMap = toToolMap(diagData);
+ state.actions = allActions.filter((action) => String(action?.id || '').trim() !== 'gh-login');
+ const allowedActionIds = new Set(
+ state.actions
+ .map((action) => String(action?.id || '').trim())
+ .filter(Boolean)
+ );
+ const persistedSkippedIds = readSkippedStepIds();
+ state.skippedActionIds = new Set(
+ Array.from(persistedSkippedIds).filter((id) => allowedActionIds.has(id))
+ );
+ if (state.actions.length > 0) {
+ const savedStep = readSavedStep();
+ setCurrentStep(savedStep, { persist: false });
+ }
+ const view = render();
+ applyOnboardingLockUI();
+ if (view.req?.coreReady) writeDismissed(false);
+
+ let hasCompletedOnboarding = readCompleted();
+ const coreReady = !!view.req?.coreReady;
+ if (isDesktopWindowsApp && coreReady && !hasCompletedOnboarding) {
+ traceOnboarding('auto-complete-triggered', {
+ coreReady,
+ hasCompletedOnboarding
+ });
+ await writeCompleted(true);
+ hasCompletedOnboarding = readCompleted();
+ }
+ const dismissed = readDismissed();
+ const shouldAutoShow = isWindowsHost && !hasCompletedOnboarding && (forceAutoShow || !readDismissed());
+ const shouldKeepVisible = open && !modal.classList.contains('hidden');
+ traceOnboarding('load-success', {
+ open,
+ forceAutoShow,
+ bootstrap,
+ explicitOpen,
+ coreReady,
+ hasCompletedOnboarding,
+ dismissed,
+ shouldAutoShow,
+ shouldKeepVisible,
+ actionIds: state.actions.map((action) => String(action?.id || '').trim()).filter(Boolean),
+ toolStates: Array.isArray(diagData?.tools)
+ ? diagData.tools.map((tool) => ({
+ id: String(tool?.id || '').trim(),
+ ok: !!tool?.ok
+ })).filter((tool) => tool.id)
+ : []
+ });
+ if (explicitOpen || shouldKeepVisible || shouldAutoShow) {
+ openModal();
+ } else {
+ setBootstrapPending(false);
+ }
+ return true;
+ } catch (err) {
+ traceOnboarding('load-failed', {
+ open,
+ forceAutoShow,
+ bootstrap,
+ explicitOpen,
+ error: String(err?.message || err)
+ });
+ summaryEl.textContent = `Dependency check failed: ${String(err?.message || err)}`;
+ listEl.innerHTML = '
Unable to load setup actions right now.
';
+ const shouldOpenOnError = explicitOpen || (open && !modal.classList.contains('hidden'));
+ if (shouldOpenOnError) openModal();
+ else if (!bootstrap) setBootstrapPending(false);
+ return false;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const stopRunPolling = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ const poller = state.actionRunPollers.get(id);
+ if (poller?.timer) clearTimeout(poller.timer);
+ state.actionRunPollers.delete(id);
+ };
+
+ const updateActionRunState = (actionId, patch = {}, { rerender = true } = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ const prev = state.actionRuns.get(id) || { actionId: id };
+ const next = {
+ ...prev,
+ ...patch,
+ actionId: id
+ };
+ state.actionRuns.set(id, next);
+ if (rerender) render();
+ return next;
+ };
+
+ const fetchSetupActionRunStatus = async ({ runId = '', actionId = '' } = {}) => {
+ const params = new URLSearchParams();
+ if (runId) params.set('runId', String(runId));
+ if (actionId) params.set('actionId', String(actionId));
+ const res = await fetch(`/api/setup-actions/run-status?${params.toString()}`);
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false || !data?.run) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
+ }
+ return data.run;
+ };
+
+ const getVerifyPolicyForAction = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (id === 'gh-login') {
+ return { attempts: 14, delayMs: 900 };
+ }
+ if (id === 'install-git' || id === 'install-node' || id === 'install-gh') {
+ return { attempts: 10, delayMs: 650 };
+ }
+ return { attempts: 8, delayMs: 650 };
+ };
+
+ const verifyActionInstalled = async (actionId, runId, options = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return false;
+ const policy = {
+ ...getVerifyPolicyForAction(id),
+ ...(options && typeof options === 'object' ? options : {})
+ };
+ const attempts = Math.max(1, Number(policy.attempts) || 1);
+ const delayMs = Math.max(250, Number(policy.delayMs) || 650);
+
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ const runState = state.actionRuns.get(id);
+ if (!runState || String(runState?.runId || '') !== String(runId || '')) return false;
+
+ updateActionRunState(id, {
+ status: 'verifying',
+ verifyAttempt: attempt,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+
+ await loadAndRender({ open: true, forceAutoShow: true });
+ const toolsMap = toToolMap(state.diagnostics);
+ if (isActionComplete(id, toolsMap)) {
+ updateActionRunState(id, {
+ status: 'verified',
+ verifyAttempt: attempts,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Dependency detected automatically.', 'success');
+ return true;
+ }
+ await sleep(delayMs);
+ }
+
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(
+ id === 'gh-login'
+ ? 'GitHub login is not detected yet. Complete sign-in in browser and try again.'
+ : 'Install finished but dependency is still missing. Review output and run again if needed.',
+ 'warning'
+ );
+ return false;
+ };
+
+ const verifyActionWithoutRun = async (actionId, options = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return false;
+ const policy = {
+ ...getVerifyPolicyForAction(id),
+ ...(options && typeof options === 'object' ? options : {})
+ };
+ const attempts = Math.max(1, Number(policy.attempts) || 1);
+ const delayMs = Math.max(250, Number(policy.delayMs) || 650);
+
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ updateActionRunState(id, {
+ status: 'verifying',
+ verifyAttempt: attempt,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+
+ await loadAndRender({ open: true, forceAutoShow: true });
+ const toolsMap = toToolMap(state.diagnostics);
+ if (isActionComplete(id, toolsMap)) {
+ updateActionRunState(id, {
+ status: 'verified',
+ verifyAttempt: attempts,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+ return true;
+ }
+ if (attempt < attempts) {
+ await sleep(delayMs);
+ }
+ }
+
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ updatedAt: new Date().toISOString()
+ });
+ return false;
+ };
+
+ const pollRunUntilDone = async (actionId, runId) => {
+ const id = String(actionId || '').trim();
+ const rid = String(runId || '').trim();
+ if (!id || !rid) return;
+
+ stopRunPolling(id);
+ const pollLoop = async () => {
+ try {
+ const run = await fetchSetupActionRunStatus({ runId: rid, actionId: id });
+ updateActionRunState(id, run);
+
+ if (String(run?.status || '').toLowerCase() === 'running') {
+ const timer = setTimeout(pollLoop, 850);
+ state.actionRunPollers.set(id, { runId: rid, timer });
+ return;
+ }
+
+ stopRunPolling(id);
+ await loadAndRender({ open: true, forceAutoShow: true });
+
+ if (String(run?.status || '').toLowerCase() === 'success') {
+ await verifyActionInstalled(id, rid);
+ return;
+ }
+
+ if (String(run?.status || '').toLowerCase() === 'failed') {
+ if (id === 'gh-login') {
+ const ghRunLines = collectRunOutputLines(run, { limit: 160 });
+ const ghRunInfo = extractGithubLoginInfo(ghRunLines);
+ const hasDeviceSignal = ghRunInfo.sawDeviceHint || !!ghRunInfo.code || /login\/device/i.test(ghRunLines.join('\n'));
+ const verifyOptions = hasDeviceSignal
+ ? { attempts: 14, delayMs: 900 }
+ : { attempts: 5, delayMs: 700 };
+ const detected = await verifyActionWithoutRun(id, verifyOptions);
+ if (detected) {
+ this.showToast('GitHub login detected automatically.', 'success');
+ return;
+ }
+
+ const runError = String(run?.error || '').trim();
+ const exitCode = Number(run?.exitCode);
+ const interrupted = exitCode === 1 || /code\s*1/i.test(runError) || /cancel|not completed/i.test(runError);
+ const missingDeviceCode = !hasDeviceSignal;
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: interrupted
+ ? 'Login was not completed in browser.'
+ : (missingDeviceCode
+ ? 'GitHub CLI did not return a one-time code.'
+ : (runError || 'Login was not completed.')),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(
+ missingDeviceCode
+ ? 'GitHub CLI did not return a one-time code. Click Start login again.'
+ : (interrupted
+ ? 'GitHub login is still not detected. If browser sign-in just finished, click Start login again.'
+ : `GitHub login failed: ${runError || 'Unknown error'}`),
+ (interrupted || missingDeviceCode) ? 'warning' : 'error'
+ );
+ return;
+ }
+ this.showToast(`Install failed: ${String(run?.error || 'Unknown error')}`, 'error');
+ }
+ } catch (err) {
+ stopRunPolling(id);
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err || 'Failed to fetch setup status'),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Install monitoring failed: ${String(err?.message || err)}`, 'error');
+ }
+ };
+
+ const timer = setTimeout(pollLoop, 250);
+ state.actionRunPollers.set(id, { runId: rid, timer });
+ };
+
+ const runBootstrapLoad = async () => {
+ if (isDesktopWindowsApp && readCompleted()) {
+ setBootstrapPending(false);
+ return;
+ }
+ 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;
+ }
+ 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}`));
+ }
+ 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 {
+ 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)) {
+ await 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', () => {
+ this.openDependencySetupWizard?.({ resetStep: true, source: 'manual-open-button' });
+ });
+ }
+ 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();
+ });
+
+ this.syncDependencySetupWizardPreferences = () => {
+ syncDesktopCompleted();
+ traceOnboarding('preferences-synced', {
+ desktopCompleted
+ });
+ if (isDesktopWindowsApp && desktopCompleted) {
+ setBootstrapPending(false);
+ }
+ applyOnboardingLockUI();
+ };
+ this.openDependencySetupWizard = ({ resetStep = true, source = 'manual-open' } = {}) => {
+ traceOnboarding('manual-open-requested', {
+ source,
+ resetStep
+ });
+ writeDismissed(false);
+ if (resetStep) setCurrentStep(0);
+ return loadAndRender({ open: true, forceAutoShow: true, explicitOpen: true });
+ };
+ this.bootstrapDependencySetupWizard = () => {
+ syncDesktopCompleted();
+ traceOnboarding('bootstrap-requested', {
+ desktopCompleted
+ });
+ if (isDesktopWindowsApp && readCompleted()) {
+ setBootstrapPending(false);
+ traceOnboarding('bootstrap-skipped', {
+ reason: 'already-completed'
+ });
+ return Promise.resolve(false);
+ }
+ return runBootstrapLoad();
+ };
+ if (this.userSettings) {
+ void this.bootstrapDependencySetupWizard();
+ }
+ }
notifyWorkflow({ type = 'info', message = '', sessionId = null, metadata = null } = {}) {
const msg = String(message || '').trim();
@@ -9353,46 +11014,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 +11074,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 +11104,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 +14417,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 +14561,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 +14569,7 @@ class ClaudeOrchestrator {
window.history.replaceState({}, document.title, window.location.pathname);
return tokenFromUrl;
}
-
+
// Check localStorage
return localStorage.getItem('claude-orchestrator-token');
}
@@ -13733,7 +15395,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 +15439,7 @@ class ClaudeOrchestrator {
window.__claudeOrchestratorFetchAuthInstalled = true;
}
-
+
// Terminal Focus Feature - Now shows only that worktree
focusTerminal(sessionId) {
// Extract worktree ID from session ID
@@ -13815,14 +15477,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 +15496,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 +15538,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 +15581,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 +15628,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 +15654,10 @@ class ClaudeOrchestrator {
}, 100);
}
}
-
+
// Clean up
this.focusedTerminalInfo = null;
-
+
// Remove ESC key listener
if (this.handleEscKey) {
document.removeEventListener('keydown', this.handleEscKey);
@@ -14005,14 +15667,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 +15811,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 +15840,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
hideClaudeStartupModal() {
const modal = document.getElementById('claude-startup-modal');
if (modal) {
@@ -14186,7 +15848,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 +15978,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 +16479,7 @@ class ClaudeOrchestrator {
document.addEventListener('keydown', handleEsc);
});
}
-
+
updateYoloState(sessionId, checked) {
// Update button styles to show YOLO is active
const buttons = [
@@ -14825,7 +16487,7 @@ class ClaudeOrchestrator {
document.getElementById(`btn-continue-${sessionId}`),
document.getElementById(`btn-resume-${sessionId}`)
];
-
+
buttons.forEach(btn => {
if (btn) {
if (checked) {
@@ -14836,25 +16498,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 +16525,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 +16545,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 {
@@ -14897,22 +16559,34 @@ class ClaudeOrchestrator {
// User Settings Methods
async loadUserSettings() {
try {
- const response = await fetch('/api/user-settings');
+ void this.traceDesktopLaunch('client.user-settings.load-start');
+ const response = await fetch('/api/user-settings', this.withLaunchTraceHeaders());
if (response.ok) {
this.userSettings = await response.json();
console.log('User settings loaded:', this.userSettings);
+ void this.traceDesktopLaunch('client.user-settings.load-success', {
+ onboardingCompleted: this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completed === true,
+ onboardingCompletedAt: this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completedAt || null
+ });
this.syncUserSettingsUI();
this.applyThemeFromUserSettings();
this.applySimpleModeConfig();
this.maybeAutoOpenSimpleMode();
this.applyUiVisibility();
+ this.syncDependencySetupWizardPreferences?.();
this.refreshBranchLabels();
this.updateTierFilterButtons();
} else {
console.error('Failed to load user settings:', response.statusText);
+ void this.traceDesktopLaunch('client.user-settings.load-failed', {
+ statusText: response.statusText || ''
+ });
}
} catch (error) {
console.error('Error loading user settings:', error);
+ void this.traceDesktopLaunch('client.user-settings.load-error', {
+ error: String(error?.message || error)
+ });
}
}
@@ -14922,16 +16596,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++) {
@@ -14942,22 +16616,35 @@ class ClaudeOrchestrator {
}
current[pathParts[pathParts.length - 1]] = value;
- const response = await fetch('/api/user-settings/global', {
+ const response = await fetch('/api/user-settings/global', this.withLaunchTraceHeaders({
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ global: newGlobal })
- });
+ }));
if (response.ok) {
const updatedSettings = await response.json();
this.userSettings = updatedSettings;
console.log('Global setting updated:', path, '=', value);
+ void this.traceDesktopLaunch('client.user-settings.global-update-success', {
+ path,
+ onboardingCompleted: updatedSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completed === true,
+ onboardingCompletedAt: updatedSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completedAt || null
+ });
this.applyUiVisibility();
} else {
console.error('Failed to update global setting:', response.statusText);
+ void this.traceDesktopLaunch('client.user-settings.global-update-failed', {
+ path,
+ statusText: response.statusText || ''
+ });
}
} catch (error) {
console.error('Error updating global setting:', error);
+ void this.traceDesktopLaunch('client.user-settings.global-update-error', {
+ path,
+ error: String(error?.message || error)
+ });
}
}
@@ -15556,7 +17243,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 +17269,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 +17287,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 +17303,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 +17329,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 +17342,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 +17374,7 @@ class ClaudeOrchestrator {
setTimeout(checkAndStart, 500); // Check again in 500ms
}
};
-
+
setTimeout(checkAndStart, 1000); // Initial delay for terminal setup
}
@@ -15696,7 +17383,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 +17489,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 +17522,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 +17530,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);
@@ -15966,7 +17653,7 @@ class ClaudeOrchestrator {
switchToWorkspace(workspaceId) {
console.log('Switching to workspace:', workspaceId);
- this.socket.emit('switch-workspace', { workspaceId });
+ this.emitWorkspaceSwitch(workspaceId, 'app.switchToWorkspace');
}
async waitForWorkspaceActive(workspaceId, { timeoutMs = 7000 } = {}) {
@@ -19679,7 +21366,7 @@ class ClaudeOrchestrator {
applyView();
return;
}
-
+
state.selectedCardId = card.id || null;
applyView();
@@ -21029,7 +22716,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 +22734,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 +22779,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 +32011,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/dashboard.js b/client/dashboard.js
index 829278c7..8b633ff2 100644
--- a/client/dashboard.js
+++ b/client/dashboard.js
@@ -3775,7 +3775,7 @@ class Dashboard {
}
// Emit workspace switch event
- this.orchestrator.socket.emit('switch-workspace', { workspaceId });
+ this.orchestrator.emitWorkspaceSwitch(workspaceId, 'dashboard.openWorkspace');
// Wait for workspace-changed event
this.orchestrator.socket.once('workspace-changed', ({ workspace, sessions }) => {
diff --git a/client/greenfield-wizard.js b/client/greenfield-wizard.js
index 49a56891..c9243076 100644
--- a/client/greenfield-wizard.js
+++ b/client/greenfield-wizard.js
@@ -897,7 +897,7 @@ class GreenfieldWizard {
// Switch to the new workspace
if (this.orchestrator && this.orchestrator.socket) {
- this.orchestrator.socket.emit('switch-workspace', { workspaceId });
+ this.orchestrator.emitWorkspaceSwitch(workspaceId, 'greenfield-wizard.openWorkspace');
}
}
}
diff --git a/client/index.html b/client/index.html
index 0605696e..99b37010 100644
--- a/client/index.html
+++ b/client/index.html
@@ -1,5 +1,6 @@
+
@@ -7,15 +8,17 @@
-
+
-
+
+
-
+
@@ -1062,4 +1104,5 @@
Notifications
+
diff --git a/client/notifications.js b/client/notifications.js
index 6fa218c3..7ec3ea9d 100644
--- a/client/notifications.js
+++ b/client/notifications.js
@@ -389,21 +389,21 @@ class NotificationManager {
// Add notification styles
const notificationStyles = document.createElement('style');
notificationStyles.textContent = `
- .empty-message {
+ .notifications-panel .empty-message {
padding: var(--space-xl);
text-align: center;
color: var(--text-secondary);
font-size: 0.875rem;
}
- .notification-header {
+ .notifications-panel .notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xs);
}
- .notification-meta {
+ .notifications-panel .notification-meta {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: var(--space-xs);
diff --git a/client/quick-links.js b/client/quick-links.js
index ead9bad4..e6e58fb8 100644
--- a/client/quick-links.js
+++ b/client/quick-links.js
@@ -682,7 +682,7 @@ class QuickLinks {
if (window.orchestrator) {
// If we need to switch workspace first
if (window.orchestrator.currentWorkspace?.id !== workspaceId) {
- window.orchestrator.socket.emit('switch-workspace', { workspaceId });
+ window.orchestrator.emitWorkspaceSwitch(workspaceId, 'quick-links.openWorkspace');
}
// Track this session access
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/client/workspace-switcher.js b/client/workspace-switcher.js
index f4835af6..fa691885 100644
--- a/client/workspace-switcher.js
+++ b/client/workspace-switcher.js
@@ -218,7 +218,7 @@ class WorkspaceSwitcher {
}
// Emit switch request
- this.orchestrator.socket.emit('switch-workspace', { workspaceId });
+ this.orchestrator.emitWorkspaceSwitch(workspaceId, 'workspace-switcher.switchWorkspace');
// Wait for workspace-changed event (handled in app.js)
this.orchestrator.socket.once('workspace-changed', () => {
@@ -267,4 +267,4 @@ class WorkspaceSwitcher {
}
// Make available globally
-window.WorkspaceSwitcher = WorkspaceSwitcher;
\ No newline at end of file
+window.WorkspaceSwitcher = WorkspaceSwitcher;
diff --git a/client/workspace-tab-manager.js b/client/workspace-tab-manager.js
index e9a33d90..d67bfdd9 100644
--- a/client/workspace-tab-manager.js
+++ b/client/workspace-tab-manager.js
@@ -173,11 +173,6 @@ class WorkspaceTabManager {
console.log(`Created tab ${tabId} for workspace ${workspace.name}`);
- // If this is the first tab, activate it
- if (this.tabs.size === 1) {
- this.switchTab(tabId);
- }
-
return tabId;
}
@@ -270,7 +265,7 @@ class WorkspaceTabManager {
const currentWorkspaceId = this.orchestrator?.currentWorkspace?.id || null;
if (this.orchestrator?.socket?.connected && targetTab.workspaceId && targetTab.workspaceId !== currentWorkspaceId) {
console.log(`Requesting backend workspace switch for tab ${tabId}: ${currentWorkspaceId} → ${targetTab.workspaceId}`);
- this.orchestrator.socket.emit('switch-workspace', { workspaceId: targetTab.workspaceId });
+ this.orchestrator.emitWorkspaceSwitch(targetTab.workspaceId, 'workspace-tab-manager.switchTab');
return;
}
diff --git a/package-lock.json b/package-lock.json
index a416406c..351617ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"express": "^4.18.2",
"http-proxy-middleware": "^3.0.5",
"multer": "^2.0.2",
- "node-pty": "^1.0.0",
+ "node-pty": "^1.1.0",
"socket.io": "^4.6.1",
"winston": "^3.11.0"
},
@@ -4773,12 +4773,6 @@
"node": ">= 10.16.0"
}
},
- "node_modules/nan": {
- "version": "2.23.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
- "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
- "license": "MIT"
- },
"node_modules/napi-postinstall": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
@@ -4811,6 +4805,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4819,13 +4819,13 @@
"license": "MIT"
},
"node_modules/node-pty": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
- "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
+ "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "nan": "^2.17.0"
+ "node-addon-api": "^7.1.0"
}
},
"node_modules/node-releases": {
diff --git a/package.json b/package.json
index d491bcea..6acf6e46 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"express": "^4.18.2",
"http-proxy-middleware": "^3.0.5",
"multer": "^2.0.2",
- "node-pty": "^1.0.0",
+ "node-pty": "^1.1.0",
"socket.io": "^4.6.1",
"winston": "^3.11.0"
},
diff --git a/patch_onboarding.py b/patch_onboarding.py
new file mode 100644
index 00000000..6d083222
--- /dev/null
+++ b/patch_onboarding.py
@@ -0,0 +1,97 @@
+import re
+
+with open('client/app.js', 'r') as f:
+ content = f.read()
+
+# We need to replace the innerHTML block inside 'const render = () => {'
+# Let's find: `listEl.innerHTML = \`` up to the closing `\`;`
+pattern = r'(listEl\.innerHTML\s*=\s*`)(.*?)(`;\n\n\s*return\s*\{\s*req,\s*steps,\s*current\s*\};\n\s*\};)'
+match = re.search(pattern, content, re.DOTALL)
+
+if not match:
+ print("Could not find listEl.innerHTML block!")
+else:
+ new_html = """
+
+ ${steps.map((step, idx) => {
+ const isActive = idx === state.currentStep;
+ const isDone = step.done;
+ let statusClass = 'stepper-upcoming';
+ if (isDone) statusClass = 'stepper-done';
+ if (isActive) statusClass = 'stepper-active';
+ return `
+
+
+ ${isActive ? `
Step ${stepNo}` : ''}
+
+
+
+ `;
+ }).join('')}
+
+
+
+
+
+
${currentTitle}
+
+
+ ${current?.done ? '
' : ''}
+
${currentDesc} ${statusText ? `(${statusText})` : ''}
+
+
+ ${isGitIdentityStep ? `
+
+ ` : ''}
+
+ ${isGhLoginStep && !current?.done ? `
+
+ ${ghLoginUiPhase === 'start' ? '
Click Start login on the right to authenticate via GitHub.
' : ''}
+ ${ghLoginUiPhase === 'wait-code' ? '
Waiting for one-time code from GitHub CLI...
' : ''}
+ ${ghLoginUiPhase === 'code' ? `
Click Open GitHub login and paste this code.
${this.escapeHtml(ghLoginCode)}
` : ''}
+
+ ` : ''}
+
+ ${shouldShowInstallerOutput ? `
+
+
${installerOutputText}
+
+ ` : ''}
+
+ ${command && !isGhLoginStep && !isGitIdentityStep && !current?.done ? `
+
+ ` : ''}
+
+
+ ${showRunButton ? `` : ''}
+ ${!isGhLoginStep && !isGitIdentityStep ? `` : ''}
+ ${isGhLoginStep && !current?.done && ghLoginUiPhase === 'code' ? `` : ''}
+
+
+
+
+
+
+
+
"""
+
+ new_content = content[:match.start(2)] + new_html + content[match.start(3):]
+ with open('client/app.js', 'w') as f:
+ f.write(new_content)
+ print("Successfully patched client/app.js")
diff --git a/scripts/ensure-pty.js b/scripts/ensure-pty.js
index ae04856c..b3179598 100644
--- a/scripts/ensure-pty.js
+++ b/scripts/ensure-pty.js
@@ -1,6 +1,41 @@
#!/usr/bin/env node
-const { execSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const NODE_BINARY = path.resolve(process.env.ORCHESTRATOR_NODE_PATH || process.env.TAURI_NODE_PATH || process.execPath || process.argv[0]);
+const NPM_CLI = path.join(path.dirname(NODE_BINARY), 'node_modules', 'npm', 'bin', 'npm-cli.js');
+
+function runCommand(command, args) {
+ const logParts = [command, ...args].join(' ');
+ const result = spawnSync(command, args, { stdio: 'inherit', cwd: process.cwd() });
+
+ if (result.error) {
+ throw result.error;
+ }
+ if (result.status !== 0) {
+ throw new Error(`Command "${logParts}" failed with exit code ${result.status}`);
+ }
+}
+
+function runNpm(command, args) {
+ if (fs.existsSync(NPM_CLI)) {
+ console.log('[node-pty] running:', NODE_BINARY, path.basename(NPM_CLI), command, ...args);
+ const result = spawnSync(NODE_BINARY, [NPM_CLI, command, ...args], { stdio: 'inherit', cwd: process.cwd() });
+ if (result.error) {
+ throw result.error;
+ }
+ if (result.status !== 0) {
+ throw new Error(`npm rebuild failed with exit code ${result.status}`);
+ }
+ return;
+ }
+
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
+ console.log('[node-pty] running:', npmCommand, command, ...args);
+ runCommand(npmCommand, [command, ...args]);
+}
function tryRequirePty() {
try {
@@ -20,11 +55,20 @@ if (firstTry === true) {
const message = firstTry && firstTry.message ? firstTry.message : String(firstTry);
console.warn('node-pty load failed, attempting rebuild...', message);
+const attemptRebuild = (command, args) => {
+ runNpm(command, args);
+};
+
try {
- execSync('npm rebuild node-pty', { stdio: 'inherit' });
+ attemptRebuild('rebuild', ['node-pty']);
} catch (error) {
- console.error('npm rebuild node-pty failed');
- process.exit(1);
+ console.warn('[node-pty] ABI mismatch rebuild failed, retrying from source:', error.message);
+ try {
+ attemptRebuild('rebuild', ['node-pty', '--build-from-source']);
+ } catch (sourceError) {
+ console.error('[node-pty] rebuild failed:', sourceError.message);
+ process.exit(1);
+ }
}
const secondTry = tryRequirePty();
diff --git a/scripts/tauri/prepare-backend-resources.js b/scripts/tauri/prepare-backend-resources.js
index d73354d5..f04bf552 100644
--- a/scripts/tauri/prepare-backend-resources.js
+++ b/scripts/tauri/prepare-backend-resources.js
@@ -37,16 +37,45 @@ function run(cmd, args, opts) {
}
}
-function runNpm(args, opts) {
- // When invoked via `npm run ...`, npm provides the JS entry path, which is
- // the most reliable cross-platform invocation target.
- const npmExecPath = String(process.env.npm_execpath || '').trim();
- if (npmExecPath) {
- run(process.execPath, [npmExecPath, ...args], opts);
+function getNodeExecutable(rawPath) {
+ const candidate = String(rawPath || '').trim();
+ if (!candidate) {
+ return '';
+ }
+ return path.resolve(candidate);
+}
+
+function getBundledNpmPath(nodeExecutable) {
+ const nodePath = getNodeExecutable(nodeExecutable);
+ if (!nodePath) {
+ return '';
+ }
+
+ const npmCli = path.join(path.dirname(nodePath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
+ if (fs.existsSync(npmCli)) {
+ return npmCli;
+ }
+
+ return '';
+}
+
+function runNpmWithNode(nodeExecutable, args, opts) {
+ const npmCli = getBundledNpmPath(nodeExecutable);
+ if (npmCli) {
+ run(nodeExecutable, [npmCli, ...args], opts);
return;
}
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
- run(npmCmd, args, opts);
+ run(npmCmd, args, {
+ env: {
+ ...process.env,
+ // Keep the node source of truth explicit for nested helpers like ensure-pty.
+ ORCHESTRATOR_NODE_PATH: getNodeExecutable(nodeExecutable),
+ TAURI_NODE_PATH: getNodeExecutable(nodeExecutable)
+ },
+ ...opts
+ });
}
function main() {
@@ -83,7 +112,17 @@ function main() {
// Default: bundle the Node runtime we’re currently running on.
// This makes `npm run tauri:build` much more “it just works” on Windows.
- const bundledNodePathRaw = bundledNodePathRawFromEnv || (shouldBundleNode ? process.execPath : '');
+ const bundledNodePathRaw = getNodeExecutable(
+ bundledNodePathRawFromEnv || (shouldBundleNode ? process.execPath : '')
+ );
+
+ const nodeEnv = bundledNodePathRaw
+ ? {
+ ...process.env,
+ ORCHESTRATOR_NODE_PATH: bundledNodePathRaw,
+ TAURI_NODE_PATH: bundledNodePathRaw
+ }
+ : process.env;
if (clean && fs.existsSync(outDir)) {
fs.rmSync(outDir, { recursive: true, force: true });
@@ -116,12 +155,32 @@ function main() {
if (installProd) {
try {
- runNpm(['ci', '--omit=dev', '--no-audit', '--no-fund'], { cwd: outDir });
+ runNpmWithNode(
+ bundledNodePathRaw || process.execPath,
+ ['ci', '--omit=dev', '--no-audit', '--no-fund'],
+ { cwd: outDir, env: nodeEnv }
+ );
} catch (error) {
// Some Windows setups have issues with `npm ci` for native modules.
// Fall back to `npm install` so contributors can still build installers.
console.warn('[tauri] NOTE: npm ci failed, falling back to npm install --omit=dev');
- runNpm(['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: outDir });
+ runNpmWithNode(
+ bundledNodePathRaw || process.execPath,
+ ['install', '--omit=dev', '--no-audit', '--no-fund'],
+ { cwd: outDir, env: nodeEnv }
+ );
+ }
+
+ try {
+ const nodePath = bundledNodePathRaw || process.execPath;
+ const ensureScriptPath = path.join(repoRoot, 'scripts', 'ensure-pty.js');
+ run(nodePath, [ensureScriptPath], {
+ cwd: outDir,
+ env: nodeEnv
+ });
+ } catch (error) {
+ console.error('[tauri] NOTE: node-pty compatibility check failed after install:', error.message);
+ throw error;
}
}
diff --git a/server/claudeVersionChecker.js b/server/claudeVersionChecker.js
index 5b1e45f7..81f08abd 100644
--- a/server/claudeVersionChecker.js
+++ b/server/claudeVersionChecker.js
@@ -15,11 +15,49 @@ const logger = winston.createLogger({
});
class ClaudeVersionChecker {
- static async checkVersion() {
- return new Promise((resolve) => {
+ static get cacheTtlMs() {
+ return 5 * 60 * 1000;
+ }
+
+ static getCachedResult() {
+ const cached = ClaudeVersionChecker.versionCache;
+ if (!cached) return null;
+ if ((Date.now() - cached.timestamp) > ClaudeVersionChecker.cacheTtlMs) {
+ ClaudeVersionChecker.versionCache = null;
+ return null;
+ }
+ return cached.result;
+ }
+
+ static setCachedResult(result) {
+ ClaudeVersionChecker.versionCache = {
+ result,
+ timestamp: Date.now()
+ };
+ return result;
+ }
+
+ static resetCache() {
+ ClaudeVersionChecker.versionCache = null;
+ ClaudeVersionChecker.versionPromise = null;
+ }
+
+ static async checkVersion({ force = false } = {}) {
+ if (!force) {
+ const cached = ClaudeVersionChecker.getCachedResult();
+ if (cached) {
+ return cached;
+ }
+ if (ClaudeVersionChecker.versionPromise) {
+ return ClaudeVersionChecker.versionPromise;
+ }
+ }
+
+ const versionPromise = new Promise((resolve) => {
const process = spawn('claude', ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'],
- timeout: 5000
+ timeout: 5000,
+ windowsHide: true
});
let stdout = '';
@@ -51,34 +89,43 @@ class ClaudeVersionChecker {
};
logger.info('Claude version check', result);
- resolve(result);
+ resolve(ClaudeVersionChecker.setCachedResult(result));
} else {
logger.warn('Could not parse Claude version', { stdout, stderr });
- resolve({
+ resolve(ClaudeVersionChecker.setCachedResult({
version: null,
isCompatible: false,
error: 'Could not parse version'
- });
+ }));
}
} else {
logger.error('Claude version check failed', { code, stderr });
- resolve({
+ resolve(ClaudeVersionChecker.setCachedResult({
version: null,
isCompatible: false,
error: `Exit code ${code}: ${stderr}`
- });
+ }));
}
});
process.on('error', (error) => {
logger.error('Claude version check error', { error: error.message, stack: error.stack });
- resolve({
+ resolve(ClaudeVersionChecker.setCachedResult({
version: null,
isCompatible: false,
error: error.message
- });
+ }));
});
});
+
+ ClaudeVersionChecker.versionPromise = versionPromise;
+ versionPromise.finally(() => {
+ if (ClaudeVersionChecker.versionPromise === versionPromise) {
+ ClaudeVersionChecker.versionPromise = null;
+ }
+ });
+
+ return versionPromise;
}
static generateUpdateInstructions(versionInfo) {
@@ -102,4 +149,4 @@ class ClaudeVersionChecker {
}
}
-module.exports = { ClaudeVersionChecker };
\ No newline at end of file
+module.exports = { ClaudeVersionChecker };
diff --git a/server/commanderService.js b/server/commanderService.js
index ded60491..d96836fb 100644
--- a/server/commanderService.js
+++ b/server/commanderService.js
@@ -62,7 +62,9 @@ class CommanderService {
try {
// Detect shell based on platform
const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
- const shellArgs = process.platform === 'win32' ? ['-NoExit'] : [];
+ const shellArgs = process.platform === 'win32'
+ ? ['-WindowStyle', 'Hidden', '-NoLogo', '-NoExit']
+ : [];
// Spawn Claude Code terminal
const ptyProcess = pty.spawn(shell, shellArgs, {
diff --git a/server/desktopLaunchTraceService.js b/server/desktopLaunchTraceService.js
new file mode 100644
index 00000000..3daff2c4
--- /dev/null
+++ b/server/desktopLaunchTraceService.js
@@ -0,0 +1,74 @@
+const winston = require('winston');
+
+const logger = winston.createLogger({
+ level: process.env.LOG_LEVEL || 'info',
+ format: winston.format.combine(
+ winston.format.timestamp(),
+ winston.format.json()
+ ),
+ transports: [
+ new winston.transports.File({
+ filename: 'logs/desktop-launch.log',
+ maxsize: 10485760,
+ maxFiles: 5
+ })
+ ]
+});
+
+function clampString(value, maxLength = 600) {
+ const text = String(value || '');
+ if (text.length <= maxLength) return text;
+ return `${text.slice(0, Math.max(0, maxLength - 16))}...[truncated]`;
+}
+
+function sanitizePayload(value, { depth = 0, seen = new WeakSet() } = {}) {
+ if (value == null) return value;
+ if (typeof value === 'boolean' || typeof value === 'number') return value;
+ if (typeof value === 'string') return clampString(value);
+ if (typeof value === 'function') return `[function:${value.name || 'anonymous'}]`;
+ if (depth >= 4) return '[depth-limit]';
+
+ if (Array.isArray(value)) {
+ const items = value.slice(0, 20).map((item) => sanitizePayload(item, { depth: depth + 1, seen }));
+ if (value.length > 20) items.push(`[+${value.length - 20} more]`);
+ return items;
+ }
+
+ if (typeof value === 'object') {
+ if (seen.has(value)) return '[circular]';
+ seen.add(value);
+ const out = {};
+ const keys = Object.keys(value).slice(0, 25);
+ keys.forEach((key) => {
+ out[key] = sanitizePayload(value[key], { depth: depth + 1, seen });
+ });
+ if (Object.keys(value).length > keys.length) {
+ out.__truncatedKeys = Object.keys(value).length - keys.length;
+ }
+ seen.delete(value);
+ return out;
+ }
+
+ return clampString(String(value));
+}
+
+function logDesktopLaunch(event, payload = {}) {
+ const name = String(event || '').trim() || 'event';
+ const safePayload = payload && typeof payload === 'object' && !Array.isArray(payload)
+ ? sanitizePayload(payload)
+ : { value: sanitizePayload(payload) };
+
+ try {
+ logger.info(name, {
+ event: name,
+ ...safePayload
+ });
+ } catch {
+ // Best-effort trace logging should never interrupt app startup.
+ }
+}
+
+module.exports = {
+ logDesktopLaunch,
+ sanitizePayload
+};
diff --git a/server/diagnosticsService.js b/server/diagnosticsService.js
index 7021c243..93e88416 100644
--- a/server/diagnosticsService.js
+++ b/server/diagnosticsService.js
@@ -9,14 +9,34 @@ const execFileAsync = util.promisify(execFile);
async function checkCommand(command, args, options = {}) {
const timeout = Number(options.timeoutMs) || 2500;
try {
- const { stdout, stderr } = await execFileAsync(command, args, {
+ const runOptions = {
timeout,
windowsHide: true,
maxBuffer: 1024 * 1024
- });
+ };
+
+ const commandStr = String(command || '').trim();
+ const argsArr = Array.isArray(args) ? args : [];
+ let result;
+ try {
+ result = await execFileAsync(commandStr, argsArr, runOptions);
+ } catch (error) {
+ const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr);
+ const shouldRetryWithCmd = isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT');
+ if (!shouldRetryWithCmd) throw error;
+ result = await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...argsArr], runOptions);
+ }
+
+ const { stdout, stderr } = result || {};
const output = String(stdout || stderr || '').trim();
const firstLine = output.split(/\r?\n/).find(Boolean) || '';
- return { ok: true, command, args, version: firstLine || null };
+ return {
+ ok: true,
+ command,
+ args,
+ version: firstLine || null,
+ output: output || null
+ };
} catch (error) {
const code = error?.code || null;
const message = String(error?.message || error || '').trim();
@@ -35,6 +55,86 @@ async function checkFirstAvailable(candidates) {
return await checkCommand(last.command, last.args, last.options);
}
+function uniqueCommandCandidates(candidates = []) {
+ const seen = new Set();
+ const out = [];
+ for (const candidate of candidates) {
+ const command = String(candidate?.command || '').trim();
+ if (!command) continue;
+ const args = Array.isArray(candidate?.args) ? candidate.args : [];
+ const key = `${command}::${JSON.stringify(args)}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.push({ command, args, options: candidate?.options });
+ }
+ return out;
+}
+
+async function checkNpmGlobalPackage(npmCommand, packageName) {
+ const npm = String(npmCommand || '').trim();
+ const pkg = String(packageName || '').trim();
+ if (!npm || !pkg) {
+ return { ok: false, error: 'Missing npm command or package name' };
+ }
+
+ const res = await checkCommand(npm, ['list', '-g', pkg, '--depth=0'], { timeoutMs: 7000 });
+ const combined = String(res?.output || res?.version || '').trim();
+ const pkgPattern = new RegExp(`${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@([^\\s]+)`, 'i');
+ const versionMatch = combined.match(pkgPattern);
+ if (!res.ok || !versionMatch?.[1]) {
+ return {
+ ok: false,
+ command: npm,
+ args: ['list', '-g', pkg, '--depth=0'],
+ error: String(res?.error || `Package ${pkg} not found in npm global list`)
+ };
+ }
+
+ return {
+ ok: true,
+ command: `npm-global:${pkg}`,
+ args: ['list', '-g', pkg, '--depth=0'],
+ version: `${pkg}@${versionMatch[1]} (npm global)`
+ };
+}
+
+async function checkGitIdentity(gitCommand, gitInstalled) {
+ const command = String(gitCommand || 'git').trim() || 'git';
+ if (!gitInstalled) {
+ return {
+ ok: false,
+ command,
+ args: ['config', '--global', '--get', 'user.name'],
+ error: 'Git is not installed'
+ };
+ }
+
+ const nameCheck = await checkCommand(command, ['config', '--global', '--get', 'user.name']);
+ const emailCheck = await checkCommand(command, ['config', '--global', '--get', 'user.email']);
+ const name = String(nameCheck?.version || '').trim();
+ const email = String(emailCheck?.version || '').trim();
+
+ if (name && email) {
+ return {
+ ok: true,
+ command,
+ args: ['config', '--global', '--get', 'user.name,user.email'],
+ version: `${name} <${email}>`
+ };
+ }
+
+ const missing = [];
+ if (!name) missing.push('user.name');
+ if (!email) missing.push('user.email');
+
+ return {
+ ok: false,
+ command,
+ args: ['config', '--global', '--get', 'user.name,user.email'],
+ error: `Missing global Git setting(s): ${missing.join(', ')}`
+ };
+}
+
function findTool(tools, id) {
if (!Array.isArray(tools)) return null;
return tools.find((tool) => String(tool?.id || '') === String(id || '')) || null;
@@ -86,47 +186,128 @@ async function collectDiagnostics() {
const tools = [];
+ const nodeCandidates = uniqueCommandCandidates([
+ { command: 'node', args: ['--version'] },
+ { command: platform === 'win32' ? 'node.exe' : 'node', args: ['--version'] },
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ { command: process.execPath || 'node', args: ['--version'] }
+ ]);
+ const nodeCheck = await checkFirstAvailable(nodeCandidates);
+ const nodeCommand = String(nodeCheck?.command || '').trim();
+ const nodeDir = nodeCommand ? path.dirname(nodeCommand) : '';
+
+ const npmCandidates = uniqueCommandCandidates([
+ { command: platform === 'win32' ? 'npm.cmd' : 'npm', args: ['--version'] },
+ platform === 'win32' ? { command: 'npm', args: ['--version'] } : null,
+ platform === 'win32' && nodeDir ? { command: path.join(nodeDir, 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'npm.cmd'), args: ['--version'] } : null
+ ]);
+ const npmCheck = await checkFirstAvailable(npmCandidates);
+
tools.push({
id: 'node',
name: 'Node.js',
- ...(await checkCommand(process.execPath || 'node', ['--version']))
+ ...nodeCheck
});
tools.push({
id: 'npm',
name: 'npm',
- ...(await checkCommand(platform === 'win32' ? 'npm.cmd' : 'npm', ['--version']))
+ ...npmCheck
});
+ const gitCandidates = uniqueCommandCandidates([
+ { command: 'git', args: ['--version'] },
+ platform === 'win32' ? { command: 'git.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null
+ ]);
+
tools.push({
id: 'git',
name: 'Git',
- ...(await checkCommand('git', ['--version']))
+ ...(await checkFirstAvailable(gitCandidates))
+ });
+ const gitTool = tools[tools.length - 1];
+ tools.push({
+ id: 'gitIdentity',
+ name: 'Git identity',
+ ...(await checkGitIdentity(gitTool?.command, !!gitTool?.ok))
});
+ const ghCandidates = uniqueCommandCandidates([
+ { command: 'gh', args: ['--version'] },
+ platform === 'win32' ? { command: 'gh.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null
+ ]);
+ const ghCheck = await checkFirstAvailable(ghCandidates);
tools.push({
id: 'gh',
name: 'GitHub CLI',
- ...(await checkCommand('gh', ['--version']))
+ ...ghCheck
});
// Auth status is the most common root cause of "0 files/commits" in PR tooling on Windows.
// We keep it lightweight: first line of `gh auth status` is enough to spot "not logged in".
+ const ghAuthCheck = ghCheck?.ok
+ ? await checkCommand(String(ghCheck.command || 'gh'), ['auth', 'status'])
+ : {
+ ok: false,
+ command: String(ghCheck?.command || 'gh'),
+ args: ['auth', 'status'],
+ error: 'GitHub CLI is not installed'
+ };
tools.push({
id: 'ghAuth',
name: 'GitHub CLI auth',
- ...(await checkCommand('gh', ['auth', 'status']))
+ ...ghAuthCheck
});
+ const claudeCandidates = uniqueCommandCandidates([
+ { command: 'claude', args: ['--version'] },
+ platform === 'win32' ? { command: 'claude.cmd', args: ['--version'] } : null,
+ platform === 'win32' ? { command: 'claude.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.claude', 'local', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Claude', 'claude.exe'), args: ['--version'] } : null
+ ]);
tools.push({
id: 'claude',
name: 'Claude Code',
- ...(await checkCommand('claude', ['--version']))
+ ...(await checkFirstAvailable(claudeCandidates))
});
+ const codexCandidates = uniqueCommandCandidates([
+ { command: 'codex', args: ['--version'] },
+ platform === 'win32' ? { command: 'codex.cmd', args: ['--version'] } : null,
+ platform === 'win32' ? { command: 'codex.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'codex.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'codex.exe'), args: ['--version'] } : null
+ ]);
+ let codexCheck = await checkFirstAvailable(codexCandidates);
+ if (!codexCheck?.ok && npmCheck?.ok) {
+ const npmPackageCheck = await checkNpmGlobalPackage(String(npmCheck.command || '').trim(), '@openai/codex');
+ if (npmPackageCheck?.ok) {
+ codexCheck = npmPackageCheck;
+ }
+ }
tools.push({
id: 'codex',
name: 'Codex CLI',
- ...(await checkCommand('codex', ['--version']))
+ ...codexCheck
});
tools.push({
diff --git a/server/githubRepoService.js b/server/githubRepoService.js
index 13a2aff3..e3027576 100644
--- a/server/githubRepoService.js
+++ b/server/githubRepoService.js
@@ -2,8 +2,12 @@ const https = require('https');
const { execFile } = require('child_process');
const winston = require('winston');
-const execFileAsync = (command, args, options) => new Promise((resolve, reject) => {
- execFile(command, args, options, (error, stdout, stderr) => {
+const execFileAsync = (command, args, options = {}) => new Promise((resolve, reject) => {
+ const runOptions = {
+ ...options,
+ windowsHide: options.windowsHide ?? true
+ };
+ execFile(command, args, runOptions, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
diff --git a/server/index.js b/server/index.js
index e3a9a62b..0ac7eafe 100644
--- a/server/index.js
+++ b/server/index.js
@@ -7,6 +7,7 @@ const fs = require('fs');
const os = require('os');
const crypto = require('crypto');
const winston = require('winston');
+const { logDesktopLaunch } = require('./desktopLaunchTraceService');
// Ensure log directory exists early (some services create file transports at require-time).
try {
@@ -42,6 +43,56 @@ const logger = winston.createLogger({
]
});
+function getLaunchTraceId(req, extras = {}) {
+ const extraTraceId = String(extras?.traceId || '').trim();
+ if (extraTraceId) return extraTraceId;
+ return String(
+ req?.get?.('x-launch-trace-id')
+ || req?.body?.traceId
+ || req?.query?.traceId
+ || ''
+ ).trim();
+}
+
+function summarizeDiagnosticTools(data) {
+ const tools = Array.isArray(data?.tools) ? data.tools : [];
+ const summary = {};
+ tools.forEach((tool) => {
+ const id = String(tool?.id || '').trim();
+ if (!id) return;
+ summary[id] = {
+ ok: !!tool?.ok,
+ version: tool?.version ? String(tool.version) : null
+ };
+ });
+ const gitOk = !!summary.git?.ok;
+ const claudeOk = !!summary.claude?.ok;
+ const codexOk = !!summary.codex?.ok;
+ return {
+ coreReady: gitOk && (claudeOk || codexOk),
+ tools: summary
+ };
+}
+
+function summarizeOnboardingState(settings) {
+ const onboarding = settings?.global?.ui?.onboarding?.desktopDependencySetup || {};
+ return {
+ completed: onboarding.completed === true,
+ completedAt: onboarding.completedAt || null
+ };
+}
+
+function traceRequest(req, event, payload = {}, extras = {}) {
+ const traceId = getLaunchTraceId(req, extras);
+ if (!traceId) return;
+ logDesktopLaunch(event, {
+ traceId,
+ method: req?.method || null,
+ path: req?.path || null,
+ ...payload
+ });
+}
+
// Import services
const { SessionManager } = require('./sessionManager');
const { StatusDetector } = require('./statusDetector');
@@ -99,6 +150,13 @@ const voiceCommandService = require('./voiceCommandService');
const whisperService = require('./whisperService');
const sessionRecoveryService = require('./sessionRecoveryService');
const { collectDiagnostics, collectFirstRunDiagnostics, collectInstallWizard, runFirstRunRepair, runFirstRunSafeRepairs } = require('./diagnosticsService');
+const {
+ getSetupActions,
+ runSetupAction,
+ getSetupActionRun,
+ getLatestSetupActionRun,
+ configureGitIdentity
+} = require('./setupActionService');
const { PluginLoaderService } = require('./pluginLoaderService');
const { SchedulerService } = require('./schedulerService');
const { PagerService } = require('./pagerService');
@@ -208,6 +266,26 @@ app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../client/index.html'));
});
+app.post('/api/desktop-launch-trace', express.json({ limit: '64kb' }), (req, res) => {
+ const traceId = getLaunchTraceId(req);
+ if (!traceId) {
+ return res.status(400).json({ ok: false, error: 'traceId is required' });
+ }
+
+ const event = String(req.body?.event || '').trim() || 'client.event';
+ logDesktopLaunch(event, {
+ traceId,
+ seq: Number.isFinite(Number(req.body?.seq)) ? Number(req.body.seq) : null,
+ source: String(req.body?.source || '').trim() || 'client',
+ details: req.body?.details && typeof req.body.details === 'object' ? req.body.details : {},
+ userAgent: req.get('user-agent') || '',
+ origin: req.get('origin') || '',
+ referer: req.get('referer') || ''
+ });
+
+ res.json({ ok: true });
+});
+
// Serve static files from client directory (but exclude index files)
const clientPath = path.join(__dirname, '../client');
logger.info(`Serving static files from: ${clientPath}`);
@@ -400,6 +478,7 @@ sessionManager.setGitHelper(gitHelper);
// Initialize workspace system
let workspaceInitialized = false;
+let workspaceSystemReady = null;
async function initializeWorkspaceSystem() {
try {
logger.info('Initializing workspace system...');
@@ -425,25 +504,28 @@ async function initializeWorkspaceSystem() {
}
// Initialize workspace system before starting server
-initializeWorkspaceSystem().then(() => {
- logger.info('Workspace system initialized');
- loadPlugins()
- .then((status) => {
- logger.info('Plugin loader finished', {
- loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0,
- failed: Array.isArray(status?.failed) ? status.failed.length : 0
- });
- })
- .catch((error) => {
- logger.error('Plugin loader failed', { error: error.message, stack: error.stack });
+workspaceSystemReady = initializeWorkspaceSystem()
+ .then(() => {
+ logger.info('Workspace system initialized');
+ return true;
+ })
+ .then(() => loadPlugins())
+ .then((status) => {
+ logger.info('Plugin loader finished', {
+ loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0,
+ failed: Array.isArray(status?.failed) ? status.failed.length : 0
});
-}).catch(error => {
- logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack });
-});
+ return true;
+ })
+ .catch(error => {
+ logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack });
+ return false;
+ });
// WebSocket connection handling
io.on('connection', (socket) => {
logger.info('Client connected', { socketId: socket.id });
+ let inFlightWorkspaceSwitchId = null;
// Send workspace info
const activeWorkspace = workspaceManager.getActiveWorkspace();
@@ -854,26 +936,69 @@ io.on('connection', (socket) => {
});
// Workspace management handlers
- socket.on('switch-workspace', async ({ workspaceId }) => {
+ socket.on('switch-workspace', async (payload = {}) => {
+ const requestedWorkspaceId = String(payload?.workspaceId || '').trim();
+ const traceId = String(payload?.traceId || '').trim() || null;
+ const source = String(payload?.source || '').trim() || 'unknown';
+ logDesktopLaunch('server.workspace-switch.received', {
+ traceId,
+ source,
+ socketId: socket.id,
+ requestedWorkspaceId,
+ currentWorkspaceId: workspaceManager.getActiveWorkspace?.()?.id || null
+ });
+ if (requestedWorkspaceId && inFlightWorkspaceSwitchId === requestedWorkspaceId) {
+ logger.info('Ignoring duplicate workspace switch request while switch is already in progress', {
+ workspaceId: requestedWorkspaceId,
+ socketId: socket.id
+ });
+ logDesktopLaunch('server.workspace-switch.ignored-duplicate', {
+ traceId,
+ source,
+ socketId: socket.id,
+ requestedWorkspaceId
+ });
+ return;
+ }
+
+ inFlightWorkspaceSwitchId = requestedWorkspaceId || null;
+ let releaseInFlightWorkspaceSwitchInFinally = true;
try {
const previous = workspaceManager.getActiveWorkspace?.() || null;
activityFeed.track('workspace.switch.requested', {
fromWorkspaceId: previous?.id || null,
- toWorkspaceId: String(workspaceId || '').trim() || null,
+ toWorkspaceId: requestedWorkspaceId || null,
socketId: socket.id
});
- logger.info('Workspace switch requested', { workspaceId });
+ logger.info('Workspace switch requested', { workspaceId: requestedWorkspaceId });
+ logDesktopLaunch('server.workspace-switch.started', {
+ traceId,
+ source,
+ socketId: socket.id,
+ previousWorkspaceId: previous?.id || null,
+ requestedWorkspaceId
+ });
- const newWorkspace = await workspaceManager.switchWorkspace(workspaceId);
+ const newWorkspace = await workspaceManager.switchWorkspace(requestedWorkspaceId);
// Ensure worktrees exist for the new workspace
logger.info('Ensuring worktrees exist for new workspace');
await worktreeHelper.ensureWorktreesExist(newWorkspace);
// Switch active workspace while preserving existing PTYs for other workspace tabs.
- const { sessions: newSessions, backlog } =
- await sessionManager.switchWorkspacePreservingSessions(newWorkspace);
+ const {
+ sessions: newSessions,
+ backlog,
+ initializePromise
+ } =
+ await sessionManager.switchWorkspacePreservingSessions(newWorkspace, {
+ reason: 'workspace-switch',
+ traceId,
+ source,
+ socketId: socket.id,
+ deferInitialize: true
+ });
// Emit success with ONLY the new workspace sessions (active workspace map)
logger.info('Sending workspace-changed event', {
@@ -898,20 +1023,86 @@ io.on('connection', (socket) => {
}
logger.info('Workspace switched successfully', { workspace: newWorkspace.name });
+ logDesktopLaunch('server.workspace-switch.completed', {
+ traceId,
+ source,
+ socketId: socket.id,
+ previousWorkspaceId: previous?.id || null,
+ workspaceId: newWorkspace?.id || null,
+ sessionCount: Object.keys(newSessions || {}).length,
+ backlogSessionCount: backlog && typeof backlog === 'object' ? Object.keys(backlog).length : 0,
+ pendingSessionInitialization: !!initializePromise
+ });
activityFeed.track('workspace.switch.completed', {
fromWorkspaceId: previous?.id || null,
toWorkspaceId: newWorkspace?.id || null,
toWorkspaceName: newWorkspace?.name || null,
socketId: socket.id
});
+
+ if (initializePromise && typeof initializePromise.then === 'function') {
+ releaseInFlightWorkspaceSwitchInFinally = false;
+ initializePromise
+ .then(() => {
+ const activeWorkspaceId = workspaceManager.getActiveWorkspace?.()?.id || null;
+ if (activeWorkspaceId !== newWorkspace?.id) {
+ return;
+ }
+ const refreshedSessions = sessionManager.getSessionStates();
+ socket.emit('sessions', refreshedSessions);
+ logDesktopLaunch('server.workspace-switch.sessions-ready', {
+ traceId,
+ source,
+ socketId: socket.id,
+ workspaceId: newWorkspace?.id || null,
+ sessionCount: Object.keys(refreshedSessions || {}).length
+ });
+ })
+ .catch((error) => {
+ logger.error('Deferred workspace session initialization failed', {
+ workspaceId: newWorkspace?.id || null,
+ error: error.message,
+ stack: error.stack
+ });
+ logDesktopLaunch('server.workspace-switch.session-init-failed', {
+ traceId,
+ source,
+ socketId: socket.id,
+ workspaceId: newWorkspace?.id || null,
+ error: error.message
+ });
+ if (workspaceManager.getActiveWorkspace?.()?.id === newWorkspace?.id) {
+ socket.emit('error', {
+ message: 'Failed to initialize workspace sessions',
+ error: error.message
+ });
+ }
+ })
+ .finally(() => {
+ if (inFlightWorkspaceSwitchId === requestedWorkspaceId) {
+ inFlightWorkspaceSwitchId = null;
+ }
+ });
+ }
} catch (error) {
+ logDesktopLaunch('server.workspace-switch.failed', {
+ traceId,
+ source,
+ socketId: socket.id,
+ requestedWorkspaceId,
+ error: error.message
+ });
activityFeed.track('workspace.switch.failed', {
- toWorkspaceId: String(workspaceId || '').trim() || null,
+ toWorkspaceId: requestedWorkspaceId || null,
socketId: socket.id,
error: error.message
});
logger.error('Failed to switch workspace', { error: error.message, stack: error.stack });
socket.emit('error', { message: 'Failed to switch workspace', error: error.message, stack: error.stack });
+ } finally {
+ if (releaseInFlightWorkspaceSwitchInFinally && inFlightWorkspaceSwitchId === requestedWorkspaceId) {
+ inFlightWorkspaceSwitchId = null;
+ }
}
});
@@ -3712,6 +3903,9 @@ app.put('/api/process/automations/pr-review/config', express.json(), async (req,
app.get('/api/user-settings', (req, res) => {
try {
const settings = userSettingsService.getAllSettings();
+ traceRequest(req, 'server.user-settings.loaded', {
+ onboarding: summarizeOnboardingState(settings)
+ });
res.json(settings);
} catch (error) {
logger.error('Failed to get user settings', { error: error.message, stack: error.stack });
@@ -3727,6 +3921,10 @@ app.put('/api/user-settings/global', express.json(), (req, res) => {
if (success) {
const updatedSettings = userSettingsService.getAllSettings();
+ traceRequest(req, 'server.user-settings.global-updated', {
+ onboarding: summarizeOnboardingState(updatedSettings),
+ updatedKeys: global && typeof global === 'object' ? Object.keys(global).slice(0, 20) : []
+ });
res.json(updatedSettings);
// Notify all clients about settings change
@@ -3971,6 +4169,7 @@ app.get('/api/agents', (req, res) => {
app.get('/api/diagnostics', async (req, res) => {
try {
const data = await collectDiagnostics();
+ traceRequest(req, 'server.diagnostics.loaded', summarizeDiagnosticTools(data));
res.json({ ok: true, ...data });
} catch (error) {
logger.error('Failed to collect diagnostics', { error: error.message, stack: error.stack });
@@ -4050,6 +4249,127 @@ 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);
+ traceRequest(req, 'server.setup-actions.loaded', {
+ platform,
+ actionIds: actions.map((action) => String(action?.id || '').trim()).filter(Boolean)
+ });
+ res.json({ ok: true, platform, actions });
+ } catch (error) {
+ logger.error('Failed to get setup actions', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to get setup actions' });
+ }
+});
+
+app.post('/api/setup-actions/run', requirePolicyAction('write'), express.json(), (req, res) => {
+ try {
+ const actionId = String(req.body?.actionId || '').trim();
+ if (!actionId) {
+ return res.status(400).json({ ok: false, error: 'actionId is required' });
+ }
+
+ const result = runSetupAction(actionId, process.platform);
+ res.json({ ok: true, ...result });
+ } catch (error) {
+ const code = String(error?.code || '');
+ const status = (code === 'unsupported_platform' || code === 'unknown_action' || code === 'not_runnable') ? 400 : 500;
+ logger.error('Failed to run setup action', { actionId: req.body?.actionId, error: error.message, stack: error.stack });
+ res.status(status).json({ ok: false, error: String(error?.message || 'Failed to run setup action') });
+ }
+});
+
+app.post('/api/setup-actions/configure-git-identity', requirePolicyAction('write'), express.json(), async (req, res) => {
+ try {
+ const name = String(req.body?.name || '').trim();
+ const email = String(req.body?.email || '').trim();
+ const result = await configureGitIdentity({ name, email }, process.platform);
+ res.json({ ok: true, ...result });
+ } catch (error) {
+ const code = String(error?.code || '');
+ const status = (
+ code === 'unsupported_platform'
+ || code === 'invalid_input'
+ || code === 'missing_git'
+ || code === 'verify_failed'
+ ) ? 400 : 500;
+ logger.error('Failed to configure git identity', {
+ error: error.message,
+ stack: error.stack
+ });
+ res.status(status).json({ ok: false, error: String(error?.message || 'Failed to configure git identity') });
+ }
+});
+
+app.get('/api/setup-actions/run-status', (req, res) => {
+ try {
+ const runId = String(req.query?.runId || '').trim();
+ const actionId = String(req.query?.actionId || '').trim();
+ const run = runId ? getSetupActionRun(runId) : getLatestSetupActionRun(actionId);
+ if (!run) {
+ return res.status(404).json({ ok: false, error: 'Setup action run not found' });
+ }
+ res.json({ ok: true, run });
+ } catch (error) {
+ logger.error('Failed to get setup action run status', {
+ runId: req.query?.runId,
+ actionId: req.query?.actionId,
+ error: error.message,
+ stack: error.stack
+ });
+ res.status(500).json({ ok: false, error: 'Failed to get setup action run status' });
+ }
+});
+
+app.post('/api/setup-actions/open-url', requirePolicyAction('write'), express.json(), (req, res) => {
+ try {
+ const rawUrl = String(req.body?.url || '').trim();
+ if (!rawUrl) {
+ return res.status(400).json({ ok: false, error: 'url is required' });
+ }
+
+ let parsed;
+ try {
+ parsed = new URL(rawUrl);
+ } catch {
+ return res.status(400).json({ ok: false, error: 'Invalid URL' });
+ }
+
+ if (!['http:', 'https:'].includes(String(parsed.protocol || '').toLowerCase())) {
+ return res.status(400).json({ ok: false, error: 'Only http/https URLs are supported' });
+ }
+
+ const targetUrl = parsed.toString();
+ const { execFile } = require('child_process');
+
+ const finish = (error) => {
+ if (error) {
+ logger.error('Failed to open setup URL', { url: targetUrl, error: error.message, stack: error.stack });
+ return res.status(500).json({ ok: false, error: 'Failed to open URL' });
+ }
+ res.json({ ok: true, opened: targetUrl });
+ };
+
+ if (process.platform === 'win32') {
+ execFile('explorer.exe', [targetUrl], { windowsHide: true }, finish);
+ return;
+ }
+
+ if (process.platform === 'darwin') {
+ execFile('open', [targetUrl], { windowsHide: true }, finish);
+ return;
+ }
+
+ execFile('xdg-open', [targetUrl], { windowsHide: true }, finish);
+ } catch (error) {
+ logger.error('Failed to open setup URL', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to open URL' });
+ }
+});
+
// Port registry API endpoints
app.get('/api/ports', (req, res) => {
try {
@@ -7836,7 +8156,13 @@ httpServer.listen(PORT, HOST, () => {
}
})();
- sessionManager.initializeSessions()
+ workspaceSystemReady
+ .then((workspaceReady) => {
+ if (!workspaceReady) {
+ return;
+ }
+ return sessionManager.initializeSessions({ reason: 'server-startup' });
+ })
.then(() => {
if (!shouldAutoEnsureDiscordServices) return;
// Don’t block server startup; just best-effort keep Services running after restarts.
diff --git a/server/sessionManager.js b/server/sessionManager.js
index d439f289..8f8cb8cf 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -13,6 +13,7 @@ const path = require('path');
const { ClaudeVersionChecker } = require('./claudeVersionChecker');
const { UserSettingsService } = require('./userSettingsService');
const { WorktreeHelper } = require('./worktreeHelper');
+const { logDesktopLaunch } = require('./desktopLaunchTraceService');
const sessionRecoveryService = require('./sessionRecoveryService');
const { parseWorktreeKey } = require('./lifecyclePolicyService');
const {
@@ -49,9 +50,9 @@ function getDefaultShell() {
// Helper function to build shell args for executing commands
function buildShellArgs(commands) {
if (process.platform === 'win32') {
- // PowerShell: join commands with ; and use -NoExit -Command to keep shell open
+ // PowerShell: hide the backing console window while keeping the shell interactive in the PTY.
const joined = Array.isArray(commands) ? commands.join('; ') : commands.replace(/&&/g, ';');
- return ['-NoExit', '-Command', joined];
+ return ['-WindowStyle', 'Hidden', '-NoLogo', '-NoExit', '-Command', joined];
} else {
// Bash: join commands with && and keep the terminal open by exec'ing into an interactive shell.
const joined = Array.isArray(commands) ? commands.join(' && ') : commands;
@@ -60,6 +61,32 @@ function buildShellArgs(commands) {
}
}
+function buildServerTerminalIntroCommands(worktreePath, label) {
+ const commands = [
+ `cd "${worktreePath}"`,
+ `echo "=== ${label} ==="`,
+ `echo "Directory: ${worktreePath}"`
+ ];
+
+ // Avoid spawning extra Git-for-Windows helper processes during terminal boot on Windows.
+ if (process.platform !== 'win32') {
+ commands.push(
+ getShellKind() === 'powershell'
+ ? `$b = git branch --show-current 2>$null; if (-not $b) { $b = 'unknown' }; Write-Output "Branch: $b"`
+ : `echo "Branch: $(git branch --show-current 2>/dev/null || echo 'unknown')"`
+ );
+ }
+
+ commands.push(
+ `echo ""`,
+ `echo "Ready to run: bun index.ts"`,
+ `echo "Available commands: bun, npm, node"`,
+ `echo ""`
+ );
+
+ return commands;
+}
+
const HOME_DIR = process.env.HOME || os.homedir();
class SessionManager extends EventEmitter {
@@ -102,6 +129,24 @@ class SessionManager extends EventEmitter {
this.worktrees = [];
}
+ shouldMonitorSessionProcesses() {
+ // Windows desktop builds were flashing visible PowerShell/conhost windows because the
+ // process-limit probe shells out once per session on an interval. Skip that probe on
+ // Windows until we have a non-console-backed process inspection path.
+ return process.platform !== 'win32';
+ }
+
+ shouldPollBranches() {
+ return process.platform !== 'win32' && this.branchRefreshMs > 0;
+ }
+
+ getGitBranchUpdateOptions(overrides = {}) {
+ return {
+ branchOnly: process.platform === 'win32',
+ ...(overrides && typeof overrides === 'object' ? overrides : {})
+ };
+ }
+
// Determine effective inactivity timeout per session (ms)
getSessionTimeout(session) {
if (!session) return this.sessionTimeout;
@@ -161,7 +206,7 @@ class SessionManager extends EventEmitter {
* - Restores (or creates) the session map for the new workspace id as `this.sessions`
* - Ensures sessions exist for the new workspace without killing old PTYs
*/
- async switchWorkspacePreservingSessions(workspace) {
+ async switchWorkspacePreservingSessions(workspace, options = {}) {
if (!workspace?.id) {
throw new Error('Workspace missing id');
}
@@ -178,7 +223,23 @@ class SessionManager extends EventEmitter {
this.workspaceSessionMaps.set(workspace.id, this.sessions);
// Ensure sessions exist for the active workspace without clearing existing ones.
- await this.initializeSessions({ preserveExisting: true });
+ const initializePromise = this.initializeSessions({
+ preserveExisting: true,
+ reason: String(options.reason || '').trim() || 'workspace-switch',
+ traceId: String(options.traceId || '').trim() || null,
+ source: String(options.source || '').trim() || null,
+ socketId: String(options.socketId || '').trim() || null
+ });
+
+ if (options.deferInitialize) {
+ return {
+ sessions: this.getSessionStates(),
+ backlog: this.getUndeliveredOutputAndMarkDelivered(),
+ initializePromise
+ };
+ }
+
+ await initializePromise;
// Return any buffered output that occurred while this workspace was inactive.
return {
@@ -243,9 +304,24 @@ class SessionManager extends EventEmitter {
async initializeSessions(options = {}) {
const preserveExisting = !!options.preserveExisting;
+ const reason = String(options.reason || '').trim() || (preserveExisting ? 'preserve-existing' : 'workspace-initialize');
+ const traceId = String(options.traceId || '').trim() || null;
+ const source = String(options.source || '').trim() || null;
+ const socketId = String(options.socketId || '').trim() || null;
// Set flag to prevent auto-restart during initialization
this.isWorkspaceSwitching = true;
+ logDesktopLaunch('session-manager.initialize.begin', {
+ traceId,
+ source,
+ socketId,
+ reason,
+ preserveExisting,
+ workspaceId: this.workspace?.id || null,
+ workspaceName: this.workspace?.name || null,
+ worktreeIds: Array.isArray(this.worktrees) ? this.worktrees.map((worktree) => worktree?.id || null) : []
+ });
+
if (!preserveExisting) {
// Clear ALL existing sessions first
logger.info('Clearing existing sessions before workspace initialization');
@@ -305,6 +381,14 @@ class SessionManager extends EventEmitter {
// If no workspace is set, skip session creation
if (!this.workspace) {
logger.warn('No workspace set, skipping session initialization');
+ logDesktopLaunch('session-manager.initialize.skipped', {
+ traceId,
+ source,
+ socketId,
+ reason,
+ preserveExisting,
+ cause: 'missing-workspace'
+ });
this.isWorkspaceSwitching = false;
return;
}
@@ -354,28 +438,21 @@ class SessionManager extends EventEmitter {
} else {
// Server terminal
command = getDefaultShell();
- const header = `=== ${terminal.repository.name}/${terminal.worktree} (${terminal.id}) ===`;
if (startCommand) {
args = buildShellArgs([
`cd "${worktree.path}"`,
- `echo "${header}"`,
+ `echo "=== ${terminal.repository.name}/${terminal.worktree} (${terminal.id}) ==="`,
`echo "Directory: ${worktree.path}"`,
`echo ""`,
startCommand
]);
} else {
- args = buildShellArgs([
- `cd "${worktree.path}"`,
- `echo "=== Server Terminal for ${terminal.repository.name}/${terminal.worktree} ==="`,
- `echo "Directory: ${worktree.path}"`,
- getShellKind() === 'powershell'
- ? `$b = git branch --show-current 2>$null; if (-not $b) { $b = 'unknown' }; Write-Output "Branch: $b"`
- : `echo "Branch: $(git branch --show-current 2>/dev/null || echo 'unknown')"`,
- `echo ""`,
- `echo "Ready to run: bun index.ts"`,
- `echo "Available commands: bun, npm, node"`,
- `echo ""`
- ]);
+ args = buildShellArgs(
+ buildServerTerminalIntroCommands(
+ worktree.path,
+ `Server Terminal for ${terminal.repository.name}/${terminal.worktree}`
+ )
+ );
}
}
@@ -387,7 +464,11 @@ class SessionManager extends EventEmitter {
worktreeId: terminal.worktree,
repositoryName: terminal.repository.name,
repositoryType: terminal.repository.type, // Add repository type for dynamic launch options
- timeoutMs
+ timeoutMs,
+ debugSource: reason,
+ launchTraceId: traceId,
+ launchSource: source,
+ launchSocketId: socketId
});
}).catch(error => {
logger.error(`Failed to initialize ${terminal.terminalType} session`, {
@@ -412,7 +493,11 @@ class SessionManager extends EventEmitter {
args: buildShellArgs(`cd "${worktree.path}"`),
cwd: worktree.path,
type: 'claude',
- worktreeId: worktree.id
+ worktreeId: worktree.id,
+ debugSource: reason,
+ launchTraceId: traceId,
+ launchSource: source,
+ launchSocketId: socketId
});
}).catch(error => {
logger.error('Failed to initialize Claude session', {
@@ -431,21 +516,14 @@ class SessionManager extends EventEmitter {
}
this.createSession(sessionId, {
command: getDefaultShell(),
- args: buildShellArgs([
- `cd "${worktree.path}"`,
- `echo "=== Server Terminal for ${worktree.id} ==="`,
- `echo "Directory: ${worktree.path}"`,
- getShellKind() === 'powershell'
- ? `$b = git branch --show-current 2>$null; if (-not $b) { $b = 'unknown' }; Write-Output "Branch: $b"`
- : `echo "Branch: $(git branch --show-current 2>/dev/null || echo 'unknown')"`,
- `echo ""`,
- `echo "Ready to run: bun index.ts"`,
- `echo "Available commands: bun, npm, node"`,
- `echo ""`
- ]),
+ args: buildShellArgs(buildServerTerminalIntroCommands(worktree.path, `Server Terminal for ${worktree.id}`)),
cwd: worktree.path,
type: 'server',
- worktreeId: worktree.id
+ worktreeId: worktree.id,
+ debugSource: reason,
+ launchTraceId: traceId,
+ launchSource: source,
+ launchSocketId: socketId
});
}).catch(error => {
logger.error('Failed to initialize server session', {
@@ -455,43 +533,30 @@ class SessionManager extends EventEmitter {
})
);
- // Add git branch update to promises array
- if (this.gitHelper) {
- sessionPromises.push(
- Promise.resolve().then(() => {
- return this.updateGitBranch(worktree.id, worktree.path);
- }).catch(error => {
- logger.error('Failed to update git branch', {
- worktree: worktree.id,
- error: error.message
- });
- })
- );
- }
}
}
- // Git branch updates for all worktrees (both traditional and mixed-repo)
- if (this.gitHelper) {
- for (const worktree of this.worktrees) {
- sessionPromises.push(
- Promise.resolve().then(() => {
- const worktreeIdForGit = worktree.worktreeId || worktree.id;
- return this.updateGitBranch(worktreeIdForGit, worktree.path);
- }).catch(error => {
- logger.error('Failed to update git branch', {
- worktree: worktree.id,
- error: error.message
- });
- })
- );
- }
- }
-
// Wait for all sessions to be created in parallel
await Promise.all(sessionPromises);
logger.info('All sessions initialized', { count: sessionPromises.length });
+ // Emit sessions as soon as terminals exist; git metadata can catch up afterward.
+ if (this.io) {
+ this.io.emit('sessions', this.getSessionStates());
+ }
+
+ logDesktopLaunch('session-manager.initialize.completed', {
+ traceId,
+ source,
+ socketId,
+ reason,
+ preserveExisting,
+ workspaceId: this.workspace?.id || null,
+ workspaceName: this.workspace?.name || null,
+ createdSessionCount: this.sessions.size,
+ sessionIds: Array.from(this.sessions.keys())
+ });
+
// Keep an authoritative reference from workspace id -> session map for tab switching.
if (this.workspace?.id) {
this.workspaceSessionMaps.set(this.workspace.id, this.sessions);
@@ -505,6 +570,22 @@ class SessionManager extends EventEmitter {
// Setup file watchers for instant branch detection
this.setupGitWatchers();
+
+ // Git branch updates for all worktrees (both traditional and mixed-repo)
+ if (this.gitHelper) {
+ const branchUpdateOptions = this.getGitBranchUpdateOptions();
+ await Promise.all(this.worktrees.map((worktree) => (
+ Promise.resolve().then(() => {
+ const worktreeIdForGit = worktree.worktreeId || worktree.id;
+ return this.updateGitBranch(worktreeIdForGit, worktree.path, false, branchUpdateOptions);
+ }).catch(error => {
+ logger.error('Failed to update git branch', {
+ worktree: worktree.id,
+ error: error.message
+ });
+ })
+ )));
+ }
}
startBranchRefresh() {
@@ -512,6 +593,11 @@ class SessionManager extends EventEmitter {
clearInterval(this.branchRefreshInterval);
}
+ if (!this.shouldPollBranches()) {
+ this.branchRefreshInterval = null;
+ return;
+ }
+
const refreshWorktrees = () => {
const refreshedPaths = new Set();
const refreshPath = (worktreeId, cwd) => {
@@ -519,7 +605,7 @@ class SessionManager extends EventEmitter {
if (!normalized) return;
if (refreshedPaths.has(normalized)) return;
refreshedPaths.add(normalized);
- this.updateGitBranch(worktreeId, normalized, true);
+ this.updateGitBranch(worktreeId, normalized, true, this.getGitBranchUpdateOptions({ branchOnly: true }));
};
this.worktrees.forEach(worktree => {
@@ -551,8 +637,6 @@ class SessionManager extends EventEmitter {
}
};
- // Do an initial refresh immediately (don't wait for the first interval tick).
- refreshWorktrees();
this.branchRefreshInterval = setInterval(refreshWorktrees, this.branchRefreshMs);
}
@@ -621,7 +705,12 @@ class SessionManager extends EventEmitter {
setTimeout(() => {
logger.debug('File watcher triggered branch update', { worktree: worktree.id });
const worktreeIdForGit = worktree.worktreeId || worktree.id;
- this.updateGitBranch(worktreeIdForGit, worktree.path, true);
+ this.updateGitBranch(
+ worktreeIdForGit,
+ worktree.path,
+ true,
+ this.getGitBranchUpdateOptions({ branchOnly: true })
+ );
}, 50);
}
});
@@ -735,10 +824,33 @@ class SessionManager extends EventEmitter {
createSession(sessionId, config) {
logger.info('Creating session', { sessionId, type: config.type });
+ logDesktopLaunch('session-manager.session.create.requested', {
+ traceId: String(config?.launchTraceId || '').trim() || null,
+ source: String(config?.launchSource || '').trim() || null,
+ socketId: String(config?.launchSocketId || '').trim() || null,
+ debugSource: String(config?.debugSource || '').trim() || null,
+ workspaceId: this.workspace?.id || null,
+ sessionId,
+ sessionType: config?.type || null,
+ worktreeId: config?.worktreeId || null,
+ cwd: config?.cwd || null,
+ command: config?.command || null,
+ args: Array.isArray(config?.args) ? config.args : []
+ });
try {
if (!pty) {
logger.error('Cannot create session - node-pty unavailable', { sessionId, type: config.type });
+ logDesktopLaunch('session-manager.session.create.failed', {
+ traceId: String(config?.launchTraceId || '').trim() || null,
+ source: String(config?.launchSource || '').trim() || null,
+ socketId: String(config?.launchSocketId || '').trim() || null,
+ debugSource: String(config?.debugSource || '').trim() || null,
+ workspaceId: this.workspace?.id || null,
+ sessionId,
+ sessionType: config?.type || null,
+ error: 'node-pty unavailable'
+ });
throw new Error('node-pty unavailable');
}
const homeDir = process.env.HOME || os.homedir();
@@ -763,6 +875,17 @@ class SessionManager extends EventEmitter {
cwd: config.cwd,
env
});
+ logDesktopLaunch('session-manager.session.spawned', {
+ traceId: String(config?.launchTraceId || '').trim() || null,
+ source: String(config?.launchSource || '').trim() || null,
+ socketId: String(config?.launchSocketId || '').trim() || null,
+ debugSource: String(config?.debugSource || '').trim() || null,
+ workspaceId: this.workspace?.id || null,
+ sessionId,
+ sessionType: config?.type || null,
+ worktreeId: config?.worktreeId || null,
+ pid: Number.isFinite(ptyProcess?.pid) ? ptyProcess.pid : null
+ });
const initialCwd = config.cwd || process.cwd();
@@ -841,6 +964,18 @@ class SessionManager extends EventEmitter {
ptyProcess.onExit(({ exitCode, signal }) => {
logger.info('Session exited', { sessionId, exitCode, signal });
const workspaceId = session.workspace || this.workspace?.id || null;
+ logDesktopLaunch('session-manager.session.exited', {
+ traceId: String(config?.launchTraceId || '').trim() || null,
+ source: String(config?.launchSource || '').trim() || null,
+ socketId: String(config?.launchSocketId || '').trim() || null,
+ debugSource: String(config?.debugSource || '').trim() || null,
+ workspaceId,
+ sessionId,
+ sessionType: session.type,
+ worktreeId: session.worktreeId || null,
+ exitCode,
+ signal
+ });
clearTimeout(session.inactivityTimer);
if (session.pendingStatusTimer) {
@@ -936,15 +1071,31 @@ class SessionManager extends EventEmitter {
});
}
- // Monitor for fork bombs (every 5 seconds)
- session.processMonitor = setInterval(() => {
- this.checkProcessLimit(session);
- // Re-evaluate status even when there is no new output, so sessions can
- // transition out of "busy" after quiet periods.
- this.refreshSessionStatus(session.id, session);
- }, 5000);
+ // Monitor for fork bombs (every 5 seconds) when the platform supports a
+ // non-intrusive process probe.
+ if (this.shouldMonitorSessionProcesses()) {
+ session.processMonitor = setInterval(() => {
+ this.checkProcessLimit(session);
+ // Re-evaluate status even when there is no new output, so sessions can
+ // transition out of "busy" after quiet periods.
+ this.refreshSessionStatus(session.id, session);
+ }, 5000);
+ } else {
+ session.processMonitor = null;
+ }
} catch (error) {
+ logDesktopLaunch('session-manager.session.create.failed', {
+ traceId: String(config?.launchTraceId || '').trim() || null,
+ source: String(config?.launchSource || '').trim() || null,
+ socketId: String(config?.launchSocketId || '').trim() || null,
+ debugSource: String(config?.debugSource || '').trim() || null,
+ workspaceId: this.workspace?.id || null,
+ sessionId,
+ sessionType: config?.type || null,
+ worktreeId: config?.worktreeId || null,
+ error: error.message
+ });
logger.error('Failed to create session', {
sessionId,
error: error.message
@@ -1145,7 +1296,12 @@ class SessionManager extends EventEmitter {
worktreeId: session.worktreeId,
delay: `${delay}ms`
});
- this.updateGitBranch(session.worktreeId, this.getSessionCwd(session), true);
+ this.updateGitBranch(
+ session.worktreeId,
+ this.getSessionCwd(session),
+ true,
+ this.getGitBranchUpdateOptions({ branchOnly: true })
+ );
}, delay);
}
@@ -1885,11 +2041,44 @@ class SessionManager extends EventEmitter {
return this.isSameOrSubpath(a, b) || this.isSameOrSubpath(b, a);
}
- async updateGitBranch(worktreeId, worktreePath, skipCache = false) {
+ getSessionsForWorktreeBranchUpdate(worktreeId, worktreePath) {
+ const sessionsToUpdate = new Set();
+
+ const claudeId = `${worktreeId}-claude`;
+ const codexId = `${worktreeId}-codex`;
+ const serverId = `${worktreeId}-server`;
+ if (this.sessions.has(claudeId)) sessionsToUpdate.add(claudeId);
+ if (this.sessions.has(codexId)) sessionsToUpdate.add(codexId);
+ if (this.sessions.has(serverId)) sessionsToUpdate.add(serverId);
+
+ const normalizedWorktreePath = this.normalizeCwdPath(worktreePath);
+ if (sessionsToUpdate.size === 0) {
+ for (const [sessionId, session] of this.sessions) {
+ if (session.worktreeId === worktreeId && session.config &&
+ this.pathsOverlap(session.config.cwd, normalizedWorktreePath)) {
+ sessionsToUpdate.add(sessionId);
+ }
+ }
+ }
+
+ if (sessionsToUpdate.size === 0) {
+ for (const [sessionId, session] of this.sessions) {
+ if (!session?.config?.cwd) continue;
+ if (!this.pathsOverlap(session.config.cwd, normalizedWorktreePath)) continue;
+ if (session.type !== 'claude' && session.type !== 'codex' && session.type !== 'server') continue;
+ sessionsToUpdate.add(sessionId);
+ }
+ }
+
+ return sessionsToUpdate;
+ }
+
+ async updateGitBranch(worktreeId, worktreePath, skipCache = false, options = {}) {
logger.info('🔄 updateGitBranch called', {
worktreeId,
path: worktreePath,
skipCache,
+ branchOnly: !!options?.branchOnly,
timestamp: new Date().toISOString()
});
@@ -1900,49 +2089,19 @@ class SessionManager extends EventEmitter {
try {
const branch = await this.gitHelper.getCurrentBranch(worktreePath, skipCache);
- const remoteUrl = await this.gitHelper.getRemoteUrl(worktreePath);
- const defaultBranch = await this.gitHelper.getDefaultBranch(worktreePath);
-
- // Check for existing PR for this branch
- const existingPR = await this.gitHelper.checkForExistingPR(remoteUrl, branch);
-
- // Update claude/codex/server sessions for this worktree
- // For mixed-repo workspaces, session IDs have workspace prefix (e.g., "mixed-terminals-work1-claude")
- // For traditional workspaces, session IDs are just worktreeId-type (e.g., "work1-claude")
- // So we need to search through sessions to find matching ones
- const sessionsToUpdate = new Set();
-
- // First try direct match (traditional workspaces)
- const claudeId = `${worktreeId}-claude`;
- const codexId = `${worktreeId}-codex`;
- const serverId = `${worktreeId}-server`;
- if (this.sessions.has(claudeId)) sessionsToUpdate.add(claudeId);
- if (this.sessions.has(codexId)) sessionsToUpdate.add(codexId);
- if (this.sessions.has(serverId)) sessionsToUpdate.add(serverId);
-
- // If no direct match, search by worktreeId AND path (mixed-repo workspaces)
- // Important: Must match both worktreeId AND path to avoid cross-contamination
- const normalizedWorktreePath = this.normalizeCwdPath(worktreePath);
- if (sessionsToUpdate.size === 0) {
- for (const [sessionId, session] of this.sessions) {
- // Check if this session belongs to the same worktree by comparing paths
- if (session.worktreeId === worktreeId && session.config &&
- this.pathsOverlap(session.config.cwd, normalizedWorktreePath)) {
- sessionsToUpdate.add(sessionId);
- }
- }
- }
+ const sessionsToUpdate = this.getSessionsForWorktreeBranchUpdate(worktreeId, worktreePath);
+ const matchingSessions = Array.from(sessionsToUpdate)
+ .map((sessionId) => this.sessions.get(sessionId))
+ .filter(Boolean);
- // Final fallback: match by path only.
- // This handles cases where the worktreeId used for watchers/refresh differs from the
- // session's stored worktreeId, but the cwd is authoritative.
- if (sessionsToUpdate.size === 0) {
- for (const [sessionId, session] of this.sessions) {
- if (!session?.config?.cwd) continue;
- if (!this.pathsOverlap(session.config.cwd, normalizedWorktreePath)) continue;
- if (session.type !== 'claude' && session.type !== 'codex' && session.type !== 'server') continue;
- sessionsToUpdate.add(sessionId);
- }
+ let remoteUrl = matchingSessions.find((session) => session?.remoteUrl)?.remoteUrl || null;
+ let defaultBranch = matchingSessions.find((session) => session?.defaultBranch)?.defaultBranch || null;
+ let existingPR = matchingSessions.find((session) => session?.existingPR)?.existingPR || null;
+
+ if (!options?.branchOnly) {
+ remoteUrl = await this.gitHelper.getRemoteUrl(worktreePath);
+ defaultBranch = await this.gitHelper.getDefaultBranch(worktreePath);
+ existingPR = await this.gitHelper.checkForExistingPR(remoteUrl, branch);
}
sessionsToUpdate.forEach(sessionId => {
@@ -2364,7 +2523,14 @@ class SessionManager extends EventEmitter {
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) => {
+ execFile(
+ 'powershell.exe',
+ ['-NoProfile', '-Command', psCmd],
+ {
+ timeout: 2000,
+ windowsHide: true
+ },
+ (err, stdout) => {
if (err) return;
const processCount = parseInt(String(stdout || '').trim(), 10);
if (!Number.isFinite(processCount)) return;
diff --git a/server/setupActionService.js b/server/setupActionService.js
new file mode 100644
index 00000000..a781c997
--- /dev/null
+++ b/server/setupActionService.js
@@ -0,0 +1,542 @@
+const crypto = require('crypto');
+const util = require('util');
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+const { spawn, execFile } = require('child_process');
+
+const execFileAsync = util.promisify(execFile);
+
+const setupActionRuns = new Map();
+const latestRunByActionId = new Map();
+const MAX_OUTPUT_LINES = 180;
+const GH_LOGIN_CODE_PATTERN = /\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i;
+const GH_LOGIN_URL_PATTERN = /https:\/\/github\.com\/login\/device(?:\S*)?/i;
+const GH_LOGIN_HINT_PATTERN = /one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i;
+
+function stripAnsi(value) {
+ return String(value || '').replace(/\u001b\[[0-9;]*m/g, '');
+}
+
+function getGhLoginDebugLogPath() {
+ const customDataDir = String(process.env.ORCHESTRATOR_DATA_DIR || '').trim();
+ if (customDataDir) {
+ return path.join(customDataDir, 'logs', 'gh-login-debug.log');
+ }
+ return path.join(os.tmpdir(), 'orchestrator-gh-login-debug.log');
+}
+
+function appendGhLoginDebugLog(event, payload = {}) {
+ const line = JSON.stringify({
+ at: new Date().toISOString(),
+ event: String(event || '').trim() || 'event',
+ ...(payload && typeof payload === 'object' ? payload : {})
+ });
+ const logPath = getGhLoginDebugLogPath();
+ try {
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
+ fs.appendFileSync(logPath, `${line}\n`, 'utf8');
+ } catch {
+ // Best-effort debug logging; never block setup flow.
+ }
+}
+
+function uniqueStrings(values = []) {
+ const seen = new Set();
+ const out = [];
+ values.forEach((value) => {
+ const item = String(value || '').trim();
+ if (!item || seen.has(item)) return;
+ seen.add(item);
+ out.push(item);
+ });
+ return out;
+}
+
+async function checkExecutable(command, args = ['--version']) {
+ const commandStr = String(command || '').trim();
+ if (!commandStr) return { ok: false, error: 'Missing command' };
+
+ const runOptions = {
+ windowsHide: true,
+ timeout: 3000,
+ maxBuffer: 1024 * 1024
+ };
+
+ try {
+ await execFileAsync(commandStr, Array.isArray(args) ? args : [], runOptions);
+ return { ok: true };
+ } catch (error) {
+ const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr);
+ if (isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT')) {
+ try {
+ await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...(Array.isArray(args) ? args : [])], runOptions);
+ return { ok: true };
+ } catch (fallbackError) {
+ return {
+ ok: false,
+ error: String(fallbackError?.message || fallbackError || 'Command check failed')
+ };
+ }
+ }
+ return {
+ ok: false,
+ error: String(error?.message || error || 'Command check failed')
+ };
+ }
+}
+
+function getGitCommandCandidates(platform = process.platform) {
+ if (platform !== 'win32') {
+ return ['git'];
+ }
+
+ return uniqueStrings([
+ 'git',
+ 'git.exe',
+ path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'),
+ path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'),
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'),
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'),
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe')
+ ]);
+}
+
+async function resolveGitCommand(platform = process.platform) {
+ const candidates = getGitCommandCandidates(platform);
+ for (const command of candidates) {
+ const check = await checkExecutable(command, ['--version']);
+ if (check.ok) return command;
+ }
+ return '';
+}
+
+async function runGitCommand(command, args = []) {
+ try {
+ const result = await execFileAsync(command, Array.isArray(args) ? args : [], {
+ windowsHide: true,
+ timeout: 9000,
+ maxBuffer: 1024 * 1024
+ });
+ return String(result?.stdout || result?.stderr || '');
+ } catch (error) {
+ const stderr = String(error?.stderr || '').trim();
+ const stdout = String(error?.stdout || '').trim();
+ const message = stderr || stdout || String(error?.message || error || 'Git command failed');
+ const err = new Error(message);
+ err.code = String(error?.code || 'git_command_failed');
+ throw err;
+ }
+}
+
+function firstNonEmptyLine(text) {
+ return String(text || '')
+ .replace(/\r/g, '')
+ .split('\n')
+ .map((line) => line.trim())
+ .find(Boolean) || '';
+}
+
+function getSetupActions(platform = process.platform) {
+ if (platform !== 'win32') {
+ return [];
+ }
+
+ return [
+ {
+ id: 'install-git',
+ title: 'Git Integration',
+ description: 'Required for repository and worktree access.',
+ command: 'winget install --id Git.Git --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://git-scm.com/download/win',
+ required: true,
+ runSupported: true
+ },
+ {
+ id: 'configure-git-identity',
+ title: 'Git Identity',
+ description: 'Set your name and email for accurate commits.',
+ command: 'git config --global user.name "Your Name"\ngit config --global user.email "you@example.com"',
+ docsUrl: 'https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup',
+ required: false,
+ optional: true,
+ runSupported: false
+ },
+ {
+ id: 'install-node',
+ title: 'Node.js LTS',
+ description: 'Required core dependency for running agents.',
+ command: 'winget install --id OpenJS.NodeJS.LTS --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://nodejs.org/en/download',
+ required: false,
+ runSupported: true
+ },
+ {
+ id: 'install-gh',
+ title: 'GitHub CLI',
+ description: 'Optional. Install now, then continue to GitHub login in the next step.',
+ command: 'winget install --id GitHub.cli --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://cli.github.com/',
+ required: false,
+ optional: true,
+ runSupported: true
+ },
+ {
+ id: 'gh-login',
+ title: 'GitHub Authentication',
+ description: 'Optional after GitHub CLI install. Sign in to enable PR and repo actions.',
+ command: [
+ "$ErrorActionPreference = 'Stop'",
+ '$env:NO_COLOR = "1"',
+ '$env:GH_PAGER = ""',
+ '$gh = ""',
+ '$cmd = Get-Command gh -ErrorAction SilentlyContinue',
+ 'if ($cmd -and $cmd.Source) { $gh = $cmd.Source }',
+ 'if (-not $gh) {',
+ ' $candidates = @(',
+ ' "$env:ProgramFiles\\GitHub CLI\\gh.exe",',
+ ' "$env:ProgramFiles(x86)\\GitHub CLI\\gh.exe",',
+ ' "$env:LOCALAPPDATA\\Programs\\GitHub CLI\\gh.exe"',
+ ' )',
+ ' foreach ($candidate in $candidates) {',
+ ' if (Test-Path $candidate) { $gh = $candidate; break }',
+ ' }',
+ '}',
+ 'if (-not $gh) { throw "GitHub CLI executable not found. Install GitHub CLI first." }',
+ '$prevErrorAction = $ErrorActionPreference',
+ '$ErrorActionPreference = "Continue"',
+ '& $gh auth status --hostname github.com *> $null',
+ '$authStatusExitCode = $LASTEXITCODE',
+ '$ErrorActionPreference = $prevErrorAction',
+ 'if ($authStatusExitCode -eq 0) { Write-Output "GitHub CLI is already authenticated."; exit 0 }',
+ 'Write-Output "Starting GitHub CLI web login..."',
+ 'Write-Output "Expect a one-time code and https://github.com/login/device below."',
+ '& $gh auth login --hostname github.com --git-protocol https --web --skip-ssh-key',
+ 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }'
+ ].join('\n'),
+ docsUrl: 'https://cli.github.com/manual/gh_auth_login',
+ required: false,
+ optional: true,
+ runSupported: true
+ },
+ {
+ id: 'install-claude',
+ title: 'Claude Code CLI',
+ description: 'Primary AI agent powered by Anthropic.',
+ command: 'winget install --id Anthropic.ClaudeCode --exact --source winget --accept-source-agreements --accept-package-agreements',
+ docsUrl: 'https://docs.claude.com/en/docs/claude-code/setup',
+ required: false,
+ optional: true,
+ runSupported: true
+ },
+ {
+ id: 'install-codex',
+ title: 'Codex CLI',
+ description: 'Alternative AI agent tool for development.',
+ command: [
+ "$ErrorActionPreference = 'Stop'",
+ '$npm = ""',
+ '$cmd = Get-Command npm -ErrorAction SilentlyContinue',
+ 'if ($cmd -and $cmd.Source) { $npm = $cmd.Source }',
+ 'if (-not $npm) {',
+ ' $candidates = @(',
+ ' "$env:ProgramFiles\\nodejs\\npm.cmd",',
+ ' "$env:ProgramFiles(x86)\\nodejs\\npm.cmd",',
+ ' "$env:LOCALAPPDATA\\Programs\\nodejs\\npm.cmd",',
+ ' "$env:APPDATA\\npm\\npm.cmd"',
+ ' )',
+ ' foreach ($candidate in $candidates) {',
+ ' if (Test-Path $candidate) { $npm = $candidate; break }',
+ ' }',
+ '}',
+ 'if (-not $npm) { throw "npm was not found. Install Node.js LTS first, then run this step again." }',
+ '& $npm install -g @openai/codex',
+ 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }'
+ ].join('\n'),
+ docsUrl: 'https://developers.openai.com/codex/cli',
+ required: false,
+ runSupported: true
+ }
+ ];
+}
+
+function getSetupActionById(actionId, platform = process.platform) {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ return getSetupActions(platform).find((action) => action.id === id) || null;
+}
+
+function createRunId(actionId) {
+ return `setup-${String(actionId || 'action')}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
+}
+
+function getRunSummary(run) {
+ if (!run) return null;
+ return {
+ runId: run.runId,
+ actionId: run.actionId,
+ title: run.title,
+ command: run.command,
+ status: run.status,
+ startedAt: run.startedAt,
+ endedAt: run.endedAt || null,
+ pid: Number.isFinite(run.pid) ? run.pid : null,
+ exitCode: Number.isInteger(run.exitCode) ? run.exitCode : null,
+ error: run.error || null,
+ output: Array.isArray(run.output) ? run.output.slice(-25) : [],
+ ghDeviceCode: run.ghDeviceCode || null,
+ ghDeviceUrl: run.ghDeviceUrl || null,
+ ghHasDeviceHint: !!run.ghHasDeviceHint,
+ ghDebugLogPath: run.ghDebugLogPath || null,
+ updatedAt: run.updatedAt || run.startedAt
+ };
+}
+
+function appendRunOutput(run, chunk, stream = 'stdout') {
+ if (!run) return;
+ const text = String(chunk || '');
+ if (!text) return;
+ const lines = text
+ .replace(/\r/g, '')
+ .split('\n')
+ .map((line) => stripAnsi(line).trimEnd())
+ .filter(Boolean);
+ if (!lines.length) return;
+ const at = new Date().toISOString();
+ lines.forEach((line) => {
+ const cleanLine = String(line || '').slice(0, 1600);
+ run.output.push({ at, stream, line: cleanLine });
+ if (run.actionId === 'gh-login') {
+ const codeMatch = cleanLine.match(GH_LOGIN_CODE_PATTERN);
+ const urlMatch = cleanLine.match(GH_LOGIN_URL_PATTERN);
+ if (codeMatch?.[1]) run.ghDeviceCode = String(codeMatch[1]).toUpperCase();
+ if (urlMatch?.[0]) run.ghDeviceUrl = String(urlMatch[0]).trim();
+ if (GH_LOGIN_HINT_PATTERN.test(cleanLine)) run.ghHasDeviceHint = true;
+ appendGhLoginDebugLog('output', {
+ runId: run.runId,
+ stream,
+ line: cleanLine
+ });
+ }
+ });
+ if (run.output.length > MAX_OUTPUT_LINES) {
+ run.output.splice(0, run.output.length - MAX_OUTPUT_LINES);
+ }
+ run.updatedAt = at;
+}
+
+function launchPowerShellCommand(action) {
+ const runId = createRunId(action.id);
+ const run = {
+ runId,
+ actionId: action.id,
+ title: action.title,
+ command: action.command,
+ status: 'running',
+ startedAt: new Date().toISOString(),
+ endedAt: null,
+ pid: null,
+ exitCode: null,
+ error: null,
+ output: [],
+ ghDeviceCode: null,
+ ghDeviceUrl: null,
+ ghHasDeviceHint: false,
+ ghDebugLogPath: action.id === 'gh-login' ? getGhLoginDebugLogPath() : null,
+ updatedAt: null
+ };
+ run.updatedAt = run.startedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_started', {
+ runId: run.runId,
+ title: run.title
+ });
+ }
+
+ setupActionRuns.set(runId, run);
+ latestRunByActionId.set(action.id, runId);
+
+ try {
+ const child = spawn(
+ 'powershell.exe',
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', String(action.command || '')],
+ {
+ detached: false,
+ windowsHide: true,
+ stdio: ['ignore', 'pipe', 'pipe']
+ }
+ );
+ run.pid = Number.isFinite(child?.pid) ? child.pid : null;
+ run.updatedAt = new Date().toISOString();
+
+ child.stdout.on('data', (chunk) => appendRunOutput(run, chunk, 'stdout'));
+ child.stderr.on('data', (chunk) => appendRunOutput(run, chunk, 'stderr'));
+
+ child.on('error', (error) => {
+ run.status = 'failed';
+ run.error = String(error?.message || error || 'Failed to launch setup action');
+ run.endedAt = new Date().toISOString();
+ run.updatedAt = run.endedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_error', {
+ runId: run.runId,
+ error: run.error
+ });
+ }
+ });
+
+ child.on('close', (code) => {
+ run.exitCode = Number.isInteger(code) ? code : null;
+ run.status = code === 0 ? 'success' : 'failed';
+ if (code !== 0 && !run.error) {
+ run.error = `Setup action exited with code ${String(code)}`;
+ }
+ run.endedAt = new Date().toISOString();
+ run.updatedAt = run.endedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_closed', {
+ runId: run.runId,
+ status: run.status,
+ exitCode: run.exitCode,
+ error: run.error || null,
+ parsedCode: run.ghDeviceCode || null,
+ parsedUrl: run.ghDeviceUrl || null,
+ sawHint: !!run.ghHasDeviceHint
+ });
+ }
+ });
+ } catch (error) {
+ run.status = 'failed';
+ run.error = String(error?.message || error || 'Failed to launch setup action');
+ run.endedAt = new Date().toISOString();
+ run.updatedAt = run.endedAt;
+ if (action.id === 'gh-login') {
+ appendGhLoginDebugLog('run_launch_failed', {
+ runId: run.runId,
+ error: run.error
+ });
+ }
+ }
+
+ return run;
+}
+
+function getSetupActionRun(runId) {
+ const key = String(runId || '').trim();
+ if (!key) return null;
+ return getRunSummary(setupActionRuns.get(key));
+}
+
+function getLatestSetupActionRun(actionId) {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ const runId = latestRunByActionId.get(id);
+ if (!runId) return null;
+ return getRunSummary(setupActionRuns.get(runId));
+}
+
+function runSetupAction(actionId, platform = process.platform) {
+ if (platform !== 'win32') {
+ const err = new Error('Setup actions are currently implemented for Windows only.');
+ err.code = 'unsupported_platform';
+ throw err;
+ }
+
+ const action = getSetupActionById(actionId, platform);
+ if (!action) {
+ const err = new Error(`Unknown setup action: ${String(actionId || '')}`);
+ err.code = 'unknown_action';
+ throw err;
+ }
+
+ if (!action.runSupported || !action.command) {
+ const err = new Error(`Action "${action.id}" cannot be launched from the app.`);
+ err.code = 'not_runnable';
+ throw err;
+ }
+
+ const latestRun = getLatestSetupActionRun(action.id);
+ if (latestRun && latestRun.status === 'running') {
+ return {
+ id: action.id,
+ title: action.title,
+ started: true,
+ alreadyRunning: true,
+ run: latestRun,
+ message: `${action.title} is already running.`
+ };
+ }
+
+ const run = launchPowerShellCommand(action);
+ const runSummary = getRunSummary(run);
+
+ return {
+ id: action.id,
+ title: action.title,
+ started: true,
+ alreadyRunning: false,
+ run: runSummary,
+ message: `Started ${action.title}. Progress updates are now tracked in onboarding.`
+ };
+}
+
+async function configureGitIdentity({ name, email } = {}, platform = process.platform) {
+ if (platform !== 'win32') {
+ const err = new Error('Git identity setup is currently implemented for Windows only.');
+ err.code = 'unsupported_platform';
+ throw err;
+ }
+
+ const normalizedName = String(name || '').trim();
+ const normalizedEmail = String(email || '').trim();
+ if (!normalizedName || !normalizedEmail) {
+ const err = new Error('Both name and email are required.');
+ err.code = 'invalid_input';
+ throw err;
+ }
+
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
+ const err = new Error('Enter a valid email address.');
+ err.code = 'invalid_input';
+ throw err;
+ }
+
+ const gitCommand = await resolveGitCommand(platform);
+ if (!gitCommand) {
+ const err = new Error('Git is not installed or not available on PATH.');
+ err.code = 'missing_git';
+ throw err;
+ }
+
+ await runGitCommand(gitCommand, ['config', '--global', 'user.name', normalizedName]);
+ await runGitCommand(gitCommand, ['config', '--global', 'user.email', normalizedEmail]);
+
+ const savedName = firstNonEmptyLine(await runGitCommand(gitCommand, ['config', '--global', '--get', 'user.name']));
+ const savedEmail = firstNonEmptyLine(await runGitCommand(gitCommand, ['config', '--global', '--get', 'user.email']));
+
+ if (!savedName || !savedEmail) {
+ const err = new Error('Git identity was saved, but verification failed.');
+ err.code = 'verify_failed';
+ throw err;
+ }
+
+ return {
+ id: 'configure-git-identity',
+ title: 'Configure Git identity',
+ ok: true,
+ gitCommand,
+ name: savedName,
+ email: savedEmail,
+ summary: `${savedName} <${savedEmail}>`,
+ message: 'Git identity saved successfully.'
+ };
+}
+
+module.exports = {
+ getSetupActions,
+ getSetupActionById,
+ runSetupAction,
+ getSetupActionRun,
+ getLatestSetupActionRun,
+ configureGitIdentity
+};
diff --git a/server/userSettingsService.js b/server/userSettingsService.js
index 1bf9c88f..d6a22707 100644
--- a/server/userSettingsService.js
+++ b/server/userSettingsService.js
@@ -153,6 +153,12 @@ class UserSettingsService {
skin: 'blue',
// 0..100 (applied as 0..1 multiplier for skin tint in CSS)
skinIntensity: 100,
+ onboarding: {
+ desktopDependencySetup: {
+ completed: false,
+ completedAt: null
+ }
+ },
visibility: {
processBanner: false,
header: {
@@ -690,6 +696,21 @@ class UserSettingsService {
};
}
+ if (ui.onboarding && typeof ui.onboarding === 'object') {
+ const defaultsOnboarding = (uiDefaults.onboarding && typeof uiDefaults.onboarding === 'object')
+ ? uiDefaults.onboarding
+ : {};
+ const nextOnboarding = ui.onboarding || {};
+ merged.global.ui.onboarding = {
+ ...defaultsOnboarding,
+ ...nextOnboarding,
+ desktopDependencySetup: {
+ ...(defaultsOnboarding.desktopDependencySetup || {}),
+ ...(nextOnboarding.desktopDependencySetup || {})
+ }
+ };
+ }
+
if (ui.visibility && typeof ui.visibility === 'object') {
const defaultsVisibility = uiDefaults.visibility || {};
const nextVisibility = ui.visibility || {};
diff --git a/server/workspaceManager.js b/server/workspaceManager.js
index 06c2aaf3..86de1346 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
},
terminals: {
pairs
diff --git a/server/worktreeHelper.js b/server/worktreeHelper.js
index d57967c2..89f92e6d 100644
--- a/server/worktreeHelper.js
+++ b/server/worktreeHelper.js
@@ -268,20 +268,24 @@ class WorktreeHelper {
executeGitCommand(command, cwd) {
return new Promise((resolve, reject) => {
const [cmd, ...args] = command.split(' ');
- const process = spawn(cmd, args, { cwd, stdio: 'pipe' });
+ const child = spawn(cmd, args, {
+ cwd,
+ stdio: 'pipe',
+ windowsHide: globalThis.process.platform === 'win32'
+ });
let stdout = '';
let stderr = '';
- process.stdout.on('data', (data) => {
+ child.stdout.on('data', (data) => {
stdout += data.toString();
});
- process.stderr.on('data', (data) => {
+ child.stderr.on('data', (data) => {
stderr += data.toString();
});
- process.on('close', (code) => {
+ child.on('close', (code) => {
if (code === 0) {
resolve(stdout.trim());
} else {
@@ -289,7 +293,7 @@ class WorktreeHelper {
}
});
- process.on('error', (error) => {
+ child.on('error', (error) => {
reject(new Error(`Failed to execute git command: ${command}\nError: ${error.message}`));
});
});
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f4ad68bb..f66beca8 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -17,6 +17,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
+tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 076c0231..6841fc0b 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};
@@ -521,6 +529,16 @@ fn main() {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "0");
tauri::Builder::default()
+ .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
+ if let Some(window) = app
+ .get_webview_window("main")
+ .or_else(|| app.webview_windows().values().next().cloned())
+ {
+ let _ = window.unminimize();
+ let _ = window.show();
+ let _ = window.set_focus();
+ }
+ }))
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
@@ -602,6 +620,8 @@ fn main() {
cmd.arg(entry);
cmd.current_dir(&data_dir);
cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::null());
+ cmd.stderr(Stdio::null());
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..e920716f 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -42,5 +42,10 @@
"resources/backend/client/*",
"resources/backend/node_modules"
]
+ },
+ "plugins": {
+ "updater": {
+ "pubkey": ""
+ }
}
}
diff --git a/tests/unit/claudeVersionChecker.test.js b/tests/unit/claudeVersionChecker.test.js
new file mode 100644
index 00000000..93af67b6
--- /dev/null
+++ b/tests/unit/claudeVersionChecker.test.js
@@ -0,0 +1,56 @@
+jest.mock('child_process', () => ({
+ spawn: jest.fn()
+}));
+
+const { spawn } = require('child_process');
+const { ClaudeVersionChecker } = require('../../server/claudeVersionChecker');
+
+describe('ClaudeVersionChecker', () => {
+ beforeEach(() => {
+ ClaudeVersionChecker.resetCache();
+ spawn.mockReset();
+ });
+
+ test('caches successful version checks', async () => {
+ let stdoutHandler = null;
+ let stderrHandler = null;
+ let closeHandler = null;
+ let errorHandler = null;
+
+ spawn.mockImplementation(() => ({
+ stdout: {
+ on: (event, handler) => {
+ if (event === 'data') stdoutHandler = handler;
+ }
+ },
+ stderr: {
+ on: (event, handler) => {
+ if (event === 'data') stderrHandler = handler;
+ }
+ },
+ on: (event, handler) => {
+ if (event === 'close') closeHandler = handler;
+ if (event === 'error') errorHandler = handler;
+ }
+ }));
+
+ const firstPromise = ClaudeVersionChecker.checkVersion();
+ expect(stdoutHandler).toBeInstanceOf(Function);
+ expect(stderrHandler).toBeInstanceOf(Function);
+ expect(closeHandler).toBeInstanceOf(Function);
+ expect(errorHandler).toBeInstanceOf(Function);
+
+ stdoutHandler(Buffer.from('claude 1.2.3'));
+ closeHandler(0);
+
+ const first = await firstPromise;
+ const second = await ClaudeVersionChecker.checkVersion();
+
+ expect(spawn).toHaveBeenCalledTimes(1);
+ expect(first).toEqual(expect.objectContaining({
+ version: '1.2.3',
+ isCompatible: true
+ }));
+ expect(second).toEqual(first);
+ });
+});
diff --git a/tests/unit/githubRepoService.listRepos.test.js b/tests/unit/githubRepoService.listRepos.test.js
index b5d7a78f..7100755c 100644
--- a/tests/unit/githubRepoService.listRepos.test.js
+++ b/tests/unit/githubRepoService.listRepos.test.js
@@ -26,6 +26,12 @@ describe('GitHubRepoService listRepos', () => {
{ nameWithOwner: 'foo/bar', name: 'bar', owner: 'foo', isPrivate: false, isFork: true, visibility: 'public' },
{ nameWithOwner: 'acme/secret', name: 'secret', owner: 'acme', isPrivate: true, isFork: false, visibility: 'private' }
]);
+ expect(execFile).toHaveBeenCalledWith(
+ 'gh',
+ ['repo', 'list', '--limit', '50', '--json', 'nameWithOwner,name,owner,isPrivate,visibility,isFork'],
+ expect.objectContaining({ windowsHide: true }),
+ expect.any(Function)
+ );
});
it('caches list results (no force)', async () => {
diff --git a/tests/unit/sessionManager.branchUpdate.test.js b/tests/unit/sessionManager.branchUpdate.test.js
index ff45c990..971bf87c 100644
--- a/tests/unit/sessionManager.branchUpdate.test.js
+++ b/tests/unit/sessionManager.branchUpdate.test.js
@@ -28,8 +28,18 @@ describe('SessionManager branch updates', () => {
sessionManager.startBranchRefresh();
jest.advanceTimersByTime(11);
- expect(updateSpy).toHaveBeenCalledWith('work2', toPlatformPath('/tmp/repo-a/work2'), true);
- expect(updateSpy).toHaveBeenCalledWith('work1', toPlatformPath('/tmp/repo-a/work1'), true);
+ expect(updateSpy).toHaveBeenCalledWith(
+ 'work2',
+ toPlatformPath('/tmp/repo-a/work2'),
+ true,
+ expect.objectContaining({ branchOnly: true })
+ );
+ expect(updateSpy).toHaveBeenCalledWith(
+ 'work1',
+ toPlatformPath('/tmp/repo-a/work1'),
+ true,
+ expect.objectContaining({ branchOnly: true })
+ );
});
test('startBranchRefresh also refreshes loose sessions (not in worktrees)', () => {
@@ -53,7 +63,44 @@ describe('SessionManager branch updates', () => {
sessionManager.startBranchRefresh();
jest.advanceTimersByTime(11);
- expect(updateSpy).toHaveBeenCalledWith('adhoc', toPlatformPath('/tmp/repo-z/adhoc'), true);
+ expect(updateSpy).toHaveBeenCalledWith(
+ 'adhoc',
+ toPlatformPath('/tmp/repo-z/adhoc'),
+ true,
+ expect.objectContaining({ branchOnly: true })
+ );
+ });
+
+ test('startBranchRefresh is disabled on Windows', () => {
+ const originalPlatform = process.platform;
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: 'win32'
+ });
+
+ try {
+ const io = { emit: jest.fn() };
+ const sessionManager = new SessionManager(io, null);
+ sessionManager.branchRefreshMs = 10;
+ sessionManager.worktrees = [
+ { id: 'work1', path: 'C:\\repo\\work1' }
+ ];
+
+ const updateSpy = jest
+ .spyOn(sessionManager, 'updateGitBranch')
+ .mockImplementation(() => Promise.resolve());
+
+ sessionManager.startBranchRefresh();
+ jest.advanceTimersByTime(25);
+
+ expect(updateSpy).not.toHaveBeenCalled();
+ expect(sessionManager.branchRefreshInterval).toBeNull();
+ } finally {
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: originalPlatform
+ });
+ }
});
test('updateGitBranch falls back to matching by cwd path', async () => {
@@ -161,4 +208,40 @@ describe('SessionManager branch updates', () => {
expect.objectContaining({ sessionId: 'repo-a-work2-codex', branch: 'feature/test' })
);
});
+
+ test('updateGitBranch can skip expensive metadata lookups', async () => {
+ const io = { emit: jest.fn() };
+ const sessionManager = new SessionManager(io, null);
+ const gitHelper = {
+ getCurrentBranch: jest.fn(async () => 'feature/test'),
+ getRemoteUrl: jest.fn(async () => 'git@github.com:owner/repo.git'),
+ getDefaultBranch: jest.fn(async () => 'main'),
+ checkForExistingPR: jest.fn(async () => 'https://github.com/owner/repo/pull/123')
+ };
+ sessionManager.setGitHelper(gitHelper);
+
+ sessionManager.sessions.set('work2-claude', {
+ id: 'work2-claude',
+ type: 'claude',
+ worktreeId: 'work2',
+ config: { cwd: '/tmp/repo-a/work2/' },
+ branch: 'unknown',
+ remoteUrl: 'https://github.com/owner/repo',
+ defaultBranch: 'main',
+ existingPR: 'https://github.com/owner/repo/pull/99'
+ });
+
+ await sessionManager.updateGitBranch('work2', '/tmp/repo-a/work2', true, { branchOnly: true });
+
+ expect(gitHelper.getCurrentBranch).toHaveBeenCalledTimes(1);
+ expect(gitHelper.getRemoteUrl).not.toHaveBeenCalled();
+ expect(gitHelper.getDefaultBranch).not.toHaveBeenCalled();
+ expect(gitHelper.checkForExistingPR).not.toHaveBeenCalled();
+ expect(sessionManager.sessions.get('work2-claude')).toEqual(expect.objectContaining({
+ branch: 'feature/test',
+ remoteUrl: 'https://github.com/owner/repo',
+ defaultBranch: 'main',
+ existingPR: 'https://github.com/owner/repo/pull/99'
+ }));
+ });
});
diff --git a/tests/unit/sessionManager.initializeSessions.test.js b/tests/unit/sessionManager.initializeSessions.test.js
new file mode 100644
index 00000000..35066caa
--- /dev/null
+++ b/tests/unit/sessionManager.initializeSessions.test.js
@@ -0,0 +1,219 @@
+jest.mock('../../server/claudeVersionChecker', () => ({
+ ClaudeVersionChecker: {
+ checkVersion: jest.fn().mockResolvedValue({
+ version: '1.0.24',
+ isCompatible: true
+ })
+ }
+}));
+
+const fs = require('fs');
+const { SessionManager } = require('../../server/sessionManager');
+
+describe('SessionManager.initializeSessions', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ test('updates each single-repo worktree branch only once during initialization', async () => {
+ jest.spyOn(fs.promises, 'access').mockResolvedValue(undefined);
+
+ const io = { emit: jest.fn() };
+ const agentManager = { getAllAgents: () => [] };
+ const sm = new SessionManager(io, agentManager);
+
+ sm.workspace = {
+ name: 'test',
+ worktrees: { enabled: false, autoCreate: false },
+ terminals: { pairs: 2 }
+ };
+ sm.worktrees = [
+ { id: 'work1', path: '/tmp/test/work1' },
+ { id: 'work2', path: '/tmp/test/work2' }
+ ];
+ sm.sessions = new Map();
+ sm.gitHelper = {};
+ sm.cleanupAllSessions = jest.fn();
+ sm.stopBranchRefresh = jest.fn();
+ sm.cleanupGitWatchers = jest.fn();
+ sm.startBranchRefresh = jest.fn();
+ sm.setupGitWatchers = jest.fn();
+ sm.createSession = jest.fn((sessionId, config) => {
+ sm.sessions.set(sessionId, {
+ id: sessionId,
+ type: config.type,
+ worktreeId: config.worktreeId,
+ config
+ });
+ });
+ sm.updateGitBranch = jest.fn().mockResolvedValue(undefined);
+
+ await sm.initializeSessions({ preserveExisting: true });
+
+ expect(sm.createSession).toHaveBeenCalledTimes(4);
+ expect(sm.updateGitBranch).toHaveBeenCalledTimes(2);
+ expect(sm.updateGitBranch).toHaveBeenNthCalledWith(
+ 1,
+ 'work1',
+ '/tmp/test/work1',
+ false,
+ expect.objectContaining({ branchOnly: false })
+ );
+ expect(sm.updateGitBranch).toHaveBeenNthCalledWith(
+ 2,
+ 'work2',
+ '/tmp/test/work2',
+ false,
+ expect.objectContaining({ branchOnly: false })
+ );
+ expect(io.emit).toHaveBeenCalledWith('sessions', expect.any(Object));
+ });
+
+ test('uses hidden PowerShell startup args for Windows sessions', async () => {
+ const originalPlatform = process.platform;
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: 'win32'
+ });
+
+ jest.spyOn(fs.promises, 'access').mockResolvedValue(undefined);
+
+ const io = { emit: jest.fn() };
+ const agentManager = { getAllAgents: () => [] };
+ const sm = new SessionManager(io, agentManager);
+
+ sm.workspace = {
+ name: 'test',
+ worktrees: { enabled: false, autoCreate: false },
+ terminals: { pairs: 1 }
+ };
+ sm.worktrees = [
+ { id: 'work1', path: 'C:\\test\\work1' }
+ ];
+ sm.sessions = new Map();
+ sm.gitHelper = {};
+ sm.cleanupAllSessions = jest.fn();
+ sm.stopBranchRefresh = jest.fn();
+ sm.cleanupGitWatchers = jest.fn();
+ sm.startBranchRefresh = jest.fn();
+ sm.setupGitWatchers = jest.fn();
+ sm.createSession = jest.fn((sessionId, config) => {
+ sm.sessions.set(sessionId, {
+ id: sessionId,
+ type: config.type,
+ worktreeId: config.worktreeId,
+ config
+ });
+ });
+ sm.updateGitBranch = jest.fn().mockResolvedValue(undefined);
+
+ try {
+ await sm.initializeSessions({ preserveExisting: true });
+ } finally {
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: originalPlatform
+ });
+ }
+
+ const claudeConfig = sm.createSession.mock.calls.find(([sessionId]) => sessionId === 'work1-claude')?.[1];
+ const serverConfig = sm.createSession.mock.calls.find(([sessionId]) => sessionId === 'work1-server')?.[1];
+
+ expect(claudeConfig).toBeTruthy();
+ expect(serverConfig).toBeTruthy();
+ expect(claudeConfig.command).toBe('powershell.exe');
+ expect(serverConfig.command).toBe('powershell.exe');
+ expect(claudeConfig.args.slice(0, 4)).toEqual(['-WindowStyle', 'Hidden', '-NoLogo', '-NoExit']);
+ expect(serverConfig.args.slice(0, 4)).toEqual(['-WindowStyle', 'Hidden', '-NoLogo', '-NoExit']);
+ expect(claudeConfig.args).toContain('-Command');
+ expect(serverConfig.args).toContain('-Command');
+ expect(sm.updateGitBranch).toHaveBeenCalledWith(
+ 'work1',
+ 'C:\\test\\work1',
+ false,
+ expect.objectContaining({ branchOnly: true })
+ );
+ });
+
+ test('skips the external process monitor on Windows', () => {
+ const originalPlatform = process.platform;
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: 'win32'
+ });
+
+ try {
+ const io = { emit: jest.fn() };
+ const agentManager = { getAllAgents: () => [] };
+ const sm = new SessionManager(io, agentManager);
+ expect(sm.shouldMonitorSessionProcesses()).toBe(false);
+ } finally {
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: originalPlatform
+ });
+ }
+ });
+
+ test('can defer workspace initialization while returning restored sessions immediately', async () => {
+ const io = { emit: jest.fn() };
+ const agentManager = { getAllAgents: () => [] };
+ const sm = new SessionManager(io, agentManager);
+
+ const previousSessions = new Map([
+ ['work0-claude', { id: 'work0-claude' }]
+ ]);
+ const restoredSessions = new Map([
+ ['work1-claude', { id: 'work1-claude' }]
+ ]);
+
+ sm.workspace = {
+ id: 'previous',
+ name: 'previous',
+ repository: { path: '/tmp/previous' },
+ worktrees: { namingPattern: 'work{n}' },
+ terminals: { pairs: 1 }
+ };
+ sm.sessions = previousSessions;
+ sm.workspaceSessionMaps.set('next', restoredSessions);
+
+ let resolveInitialization;
+ sm.initializeSessions = jest.fn(() => new Promise((resolve) => {
+ resolveInitialization = resolve;
+ }));
+ sm.getSessionStates = jest.fn(() => ({
+ 'work1-claude': { id: 'work1-claude' }
+ }));
+ sm.getUndeliveredOutputAndMarkDelivered = jest.fn(() => ({
+ 'work1-claude': 'buffered output'
+ }));
+
+ const result = await sm.switchWorkspacePreservingSessions({
+ id: 'next',
+ name: 'next',
+ repository: { path: '/tmp/next' },
+ worktrees: { namingPattern: 'work{n}' },
+ terminals: { pairs: 1 }
+ }, {
+ deferInitialize: true,
+ reason: 'workspace-switch'
+ });
+
+ expect(sm.workspaceSessionMaps.get('previous')).toBe(previousSessions);
+ expect(sm.sessions).toBe(restoredSessions);
+ expect(result.sessions).toEqual({
+ 'work1-claude': { id: 'work1-claude' }
+ });
+ expect(result.backlog).toEqual({
+ 'work1-claude': 'buffered output'
+ });
+ expect(result.initializePromise).toBeInstanceOf(Promise);
+ expect(sm.initializeSessions).toHaveBeenCalledWith(expect.objectContaining({
+ preserveExisting: true,
+ reason: 'workspace-switch'
+ }));
+
+ resolveInitialization();
+ await result.initializePromise;
+ });
+});
diff --git a/tests/unit/userSettingsDefaults.test.js b/tests/unit/userSettingsDefaults.test.js
index e2b29f2b..5e64ca90 100644
--- a/tests/unit/userSettingsDefaults.test.js
+++ b/tests/unit/userSettingsDefaults.test.js
@@ -46,6 +46,14 @@ describe('UserSettingsService defaults', () => {
expect(defaults.global.ui.workflow.notifications.mode).toBeTruthy();
});
+ test('includes desktop onboarding defaults', () => {
+ const defaults = UserSettingsService.prototype.getDefaultSettings.call({});
+ const onboarding = defaults?.global?.ui?.onboarding?.desktopDependencySetup;
+ expect(onboarding).toBeTruthy();
+ expect(onboarding.completed).toBe(false);
+ expect(onboarding.completedAt).toBeNull();
+ });
+
test('includes ui.worktrees auto-create defaults', () => {
const defaults = UserSettingsService.prototype.getDefaultSettings.call({});
expect(defaults?.global?.ui?.worktrees).toBeTruthy();
@@ -186,6 +194,10 @@ describe('UserSettingsService defaults', () => {
// Does not drop workflow defaults when only mode is provided.
expect(merged.global.ui.workflow.focus).toBeTruthy();
expect(merged.global.ui.workflow.notifications).toBeTruthy();
+ // Keeps onboarding defaults while allowing desktop completion to persist.
+ expect(merged.global.ui.onboarding).toBeTruthy();
+ expect(merged.global.ui.onboarding.desktopDependencySetup.completed).toBe(false);
+ expect(merged.global.ui.onboarding.desktopDependencySetup.completedAt).toBeNull();
// Keeps ui.skin when provided.
expect(merged.global.ui.skin).toBe('blue');
// Keeps simpleMode defaults while allowing partial override.
@@ -214,4 +226,23 @@ describe('UserSettingsService defaults', () => {
expect(merged.global.pager.doneCheck.enabled).toBe(true);
expect(typeof merged.global.pager.doneCheck.token).toBe('string');
});
+
+ test('mergeSettings deep-merges desktop onboarding state', () => {
+ const defaults = UserSettingsService.prototype.getDefaultSettings.call({});
+ const merged = UserSettingsService.prototype.mergeSettings.call({}, defaults, {
+ global: {
+ ui: {
+ onboarding: {
+ desktopDependencySetup: {
+ completed: true,
+ completedAt: '2026-03-06T00:00:00.000Z'
+ }
+ }
+ }
+ }
+ });
+
+ expect(merged.global.ui.onboarding.desktopDependencySetup.completed).toBe(true);
+ expect(merged.global.ui.onboarding.desktopDependencySetup.completedAt).toBe('2026-03-06T00:00:00.000Z');
+ });
});
diff --git a/windows-onboarding-clean-main-d696ca7.zip b/windows-onboarding-clean-main-d696ca7.zip
new file mode 100644
index 00000000..8dca4313
Binary files /dev/null and b/windows-onboarding-clean-main-d696ca7.zip differ
diff --git a/windows-onboarding-clean-main.zip b/windows-onboarding-clean-main.zip
new file mode 100644
index 00000000..7bba7a47
Binary files /dev/null and b/windows-onboarding-clean-main.zip differ